effect-cf 0.0.1

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/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "effect-cf",
3
+ "version": "0.0.1",
4
+ "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/danieljvdm/effect-cf.git",
9
+ "directory": "packages/effect-cf"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "type": "module",
16
+ "types": "./src/index.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.ts",
20
+ "import": "./dist/index.mjs",
21
+ "default": "./dist/index.mjs"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "build": "vp pack",
30
+ "dev": "vp pack --watch",
31
+ "test": "vp test",
32
+ "check": "vp check",
33
+ "prepublishOnly": "vp run build"
34
+ },
35
+ "devDependencies": {
36
+ "@cloudflare/vitest-pool-workers": "^0.15.0",
37
+ "@cloudflare/workers-types": "^4.20260421.1",
38
+ "@effect/vitest": "catalog:",
39
+ "effect": "catalog:",
40
+ "typescript": "^6.0.2",
41
+ "vite-plus": "0.1.19",
42
+ "vitest": "catalog:"
43
+ },
44
+ "peerDependencies": {
45
+ "effect": "^4.0.0-beta.57"
46
+ }
47
+ }
package/src/Binding.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { Context, Data, Effect, Layer } from "effect";
2
+
3
+ import { WorkerEnvironment } from "./Environment";
4
+ import type { WorkerEnv } from "./Environment";
5
+
6
+ /** Internal type id marker used by binding helper services. */
7
+ export const TypeId = "effect-cf/Binding" as const;
8
+
9
+ /** Error raised when a configured binding does not exist on `env`. */
10
+ export class BindingNotFoundError extends Data.TaggedError("BindingNotFoundError")<{
11
+ readonly binding: string;
12
+ }> {}
13
+
14
+ /** Error raised when a binding exists but does not match the expected shape. */
15
+ export class BindingValidationError extends Data.TaggedError("BindingValidationError")<{
16
+ readonly binding: string;
17
+ }> {}
18
+
19
+ const isPropertyTarget = (value: unknown): value is object =>
20
+ (typeof value === "object" || typeof value === "function") && value !== null;
21
+
22
+ const getBinding = <Resource>(
23
+ env: WorkerEnv,
24
+ binding: string,
25
+ isResource: (value: unknown) => value is Resource,
26
+ ): Effect.Effect<Resource, BindingNotFoundError | BindingValidationError> =>
27
+ Effect.gen(function* () {
28
+ if (!isPropertyTarget(env)) {
29
+ return yield* Effect.fail(new BindingValidationError({ binding }));
30
+ }
31
+
32
+ const resource = Reflect.get(env, binding);
33
+
34
+ if (resource === undefined) {
35
+ return yield* Effect.fail(new BindingNotFoundError({ binding }));
36
+ }
37
+
38
+ if (!isResource(resource)) {
39
+ return yield* Effect.fail(new BindingValidationError({ binding }));
40
+ }
41
+
42
+ return resource;
43
+ });
44
+
45
+ /**
46
+ * Creates a Context tag + layer for reading and validating a Cloudflare binding.
47
+ */
48
+ export const Service =
49
+ <Self>() =>
50
+ <Id extends string, Resource>(
51
+ id: Id,
52
+ binding: string,
53
+ isResource: (value: unknown) => value is Resource,
54
+ ) => {
55
+ const tag = Context.Service<Self, Resource>()(id);
56
+
57
+ const layer = Layer.effect(
58
+ tag,
59
+ Effect.gen(function* () {
60
+ const env = yield* WorkerEnvironment;
61
+ return yield* getBinding(env, binding, isResource);
62
+ }),
63
+ );
64
+
65
+ return Object.assign(tag, {
66
+ [TypeId]: TypeId,
67
+ binding,
68
+ layer,
69
+ });
70
+ };
@@ -0,0 +1,220 @@
1
+ import { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
2
+ import { Effect, Layer, ManagedRuntime, type Scope } from "effect";
3
+
4
+ import { NativeRequest } from "./Worker";
5
+ import { WorkerEnvironment, type WorkerEnv } from "./Environment";
6
+ import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState";
7
+ import * as DurableObjectDefinition from "./DurableObjectDefinition";
8
+ import * as Entrypoint from "./internal/Entrypoint";
9
+
10
+ const reservedMethodNames = new Set<string>([
11
+ "constructor",
12
+ "dup",
13
+ "fetch",
14
+ "alarm",
15
+ "webSocketMessage",
16
+ "webSocketClose",
17
+ "webSocketError",
18
+ ]);
19
+
20
+ type RuntimeContext<ROut> = DurableObjectState | WorkerEnvironment | ROut;
21
+
22
+ type HandlerContext<ROut> = RuntimeContext<ROut> | Scope.Scope;
23
+
24
+ type FetchContext<ROut> = HandlerContext<ROut> | NativeRequest;
25
+
26
+ const RunSymbol = Symbol.for("effect-cf/DurableObject/run");
27
+
28
+ /**
29
+ * Effect type for Durable Object lifecycle and RPC handlers.
30
+ */
31
+ export type DurableObjectHandler<ROut, A = unknown> = Effect.Effect<
32
+ A,
33
+ unknown,
34
+ HandlerContext<ROut>
35
+ >;
36
+
37
+ /**
38
+ * Shape of Durable Object RPC handlers passed to {@link make}.
39
+ */
40
+ export type DurableObjectRpc<ROut> = Record<
41
+ string,
42
+ (...args: Array<any>) => DurableObjectHandler<ROut>
43
+ >;
44
+
45
+ export type DurableObjectRpcShape<Rpc extends DurableObjectRpc<ROut>, ROut> = {
46
+ readonly [Key in keyof Rpc]: Rpc[Key] extends (
47
+ ...args: infer Args
48
+ ) => Effect.Effect<infer A, unknown, HandlerContext<ROut>>
49
+ ? (...args: Args) => Promise<A>
50
+ : never;
51
+ };
52
+
53
+ export type RpcHandlers<ROut, Api> = {
54
+ readonly [Key in keyof Api as Key extends keyof CloudflareDurableObject<WorkerEnv>
55
+ ? never
56
+ : Key extends string
57
+ ? [Api[Key]] extends [never]
58
+ ? never
59
+ : Api[Key] extends (...args: Array<any>) => Promise<unknown>
60
+ ? Key
61
+ : never
62
+ : never]: Api[Key] extends (...args: infer Args) => Promise<infer A>
63
+ ? (...args: Args) => DurableObjectHandler<ROut, A>
64
+ : never;
65
+ };
66
+
67
+ /**
68
+ * Options for creating a Durable Object class backed by Effect handlers.
69
+ */
70
+ export interface DurableObjectOptions<ROut, Rpc extends DurableObjectRpc<ROut>> {
71
+ /** Optional RPC methods exposed as Durable Object instance methods. */
72
+ readonly rpc?: Rpc;
73
+ /** Optional fetch handler for HTTP/WebSocket requests. */
74
+ readonly fetch?: Effect.Effect<Response, unknown, FetchContext<ROut>>;
75
+ readonly alarm?: (
76
+ alarmInfo?: globalThis.AlarmInvocationInfo,
77
+ ) => Effect.Effect<void, unknown, HandlerContext<ROut>>;
78
+ readonly webSocketMessage?: (
79
+ socket: WebSocket,
80
+ message: string | ArrayBuffer,
81
+ ) => Effect.Effect<void, unknown, HandlerContext<ROut>>;
82
+ readonly webSocketClose?: (
83
+ socket: WebSocket,
84
+ code: number,
85
+ reason: string,
86
+ wasClean: boolean,
87
+ ) => Effect.Effect<void, unknown, HandlerContext<ROut>>;
88
+ readonly webSocketError?: (
89
+ socket: WebSocket,
90
+ error: unknown,
91
+ ) => Effect.Effect<void, unknown, HandlerContext<ROut>>;
92
+ }
93
+
94
+ /**
95
+ * Cloudflare `DurableObject` constructor produced by {@link make}.
96
+ */
97
+ export type DurableObjectClass<Rpc extends DurableObjectRpc<ROut>, ROut> = new (
98
+ state: globalThis.DurableObjectState,
99
+ env: WorkerEnv,
100
+ ) => CloudflareDurableObject<WorkerEnv> & DurableObjectRpcShape<Rpc, ROut>;
101
+
102
+ /**
103
+ * Creates a Durable Object class backed by a single managed Effect runtime.
104
+ */
105
+ export const make = <
106
+ ROut,
107
+ LayerError,
108
+ const Rpc extends DurableObjectRpc<ROut> = Record<never, never>,
109
+ >(
110
+ layer: Layer.Layer<ROut, LayerError, DurableObjectState | WorkerEnvironment>,
111
+ options: DurableObjectOptions<ROut, Rpc> = {},
112
+ ): DurableObjectClass<Rpc, ROut> => {
113
+ class EffectDurableObject extends CloudflareDurableObject<WorkerEnv> {
114
+ readonly runtime: ManagedRuntime.ManagedRuntime<RuntimeContext<ROut>, LayerError>;
115
+
116
+ constructor(state: globalThis.DurableObjectState, env: WorkerEnv) {
117
+ super(state, env);
118
+
119
+ const services = Layer.mergeAll(
120
+ Layer.succeed(DurableObjectState, fromDurableObjectState(state)),
121
+ Layer.succeed(WorkerEnvironment, env),
122
+ );
123
+
124
+ const runtimeLayer = Entrypoint.provideEntrypointServices(layer, services);
125
+
126
+ this.runtime = ManagedRuntime.make(runtimeLayer);
127
+ }
128
+
129
+ [RunSymbol]<A, E>(effect: Effect.Effect<A, E, HandlerContext<ROut>>): Promise<A> {
130
+ return this.runtime.runPromise(Effect.scoped(effect));
131
+ }
132
+
133
+ fetch(request: Request): Promise<Response> {
134
+ const fetchHandler = options.fetch;
135
+
136
+ if (fetchHandler === undefined) {
137
+ return Promise.resolve(new Response("Not Found", { status: 404 }));
138
+ }
139
+
140
+ return this[RunSymbol](Effect.provideService(fetchHandler, NativeRequest, request));
141
+ }
142
+
143
+ alarm(alarmInfo?: globalThis.AlarmInvocationInfo): Promise<void> | void {
144
+ if (options.alarm !== undefined) {
145
+ return this[RunSymbol](options.alarm(alarmInfo));
146
+ }
147
+ }
148
+
149
+ webSocketMessage(socket: WebSocket, message: string | ArrayBuffer): Promise<void> | void {
150
+ if (options.webSocketMessage !== undefined) {
151
+ return this[RunSymbol](options.webSocketMessage(socket, message));
152
+ }
153
+ }
154
+
155
+ webSocketClose(
156
+ socket: WebSocket,
157
+ code: number,
158
+ reason: string,
159
+ wasClean: boolean,
160
+ ): Promise<void> | void {
161
+ if (options.webSocketClose !== undefined) {
162
+ return this[RunSymbol](options.webSocketClose(socket, code, reason, wasClean));
163
+ }
164
+ }
165
+
166
+ webSocketError(socket: WebSocket, error: unknown): Promise<void> | void {
167
+ if (options.webSocketError !== undefined) {
168
+ return this[RunSymbol](options.webSocketError(socket, error));
169
+ }
170
+ }
171
+ }
172
+
173
+ Entrypoint.defineEntrypointRpcMethods<EffectDurableObject>(
174
+ "Durable Object",
175
+ EffectDurableObject.prototype,
176
+ options.rpc,
177
+ reservedMethodNames,
178
+ (self, effect) => self[RunSymbol](effect),
179
+ );
180
+
181
+ return Entrypoint.assumeEntrypointClass<DurableObjectClass<Rpc, ROut>>(EffectDurableObject);
182
+ };
183
+
184
+ export const Tag = DurableObjectDefinition.Tag;
185
+
186
+ export const method = DurableObjectDefinition.method;
187
+
188
+ export const implement = DurableObjectDefinition.implement;
189
+
190
+ export type Definition<
191
+ Id extends string = string,
192
+ Methods extends DurableObjectDefinition.Definition.Any["methods"] =
193
+ DurableObjectDefinition.Definition.Any["methods"],
194
+ > = DurableObjectDefinition.Definition<Id, Methods>;
195
+
196
+ export namespace Definition {
197
+ export type Any = DurableObjectDefinition.Definition.Any;
198
+ }
199
+
200
+ export type ServerApi<Self extends DurableObjectDefinition.Definition.Any> =
201
+ DurableObjectDefinition.ServerApi<Self>;
202
+
203
+ export type Api<Self extends DurableObjectDefinition.Definition.Any> =
204
+ DurableObjectDefinition.Api<Self>;
205
+
206
+ export type Handlers<
207
+ ROut,
208
+ Self extends DurableObjectDefinition.Definition.Any,
209
+ > = DurableObjectDefinition.Handlers<ROut, Self>;
210
+
211
+ export type Options<
212
+ ROut,
213
+ Self extends DurableObjectDefinition.Definition.Any,
214
+ > = DurableObjectDefinition.Options<ROut, Self>;
215
+
216
+ export type HandlerEffect<
217
+ ROut,
218
+ Self extends DurableObjectDefinition.Definition.Any,
219
+ Key extends keyof Self["methods"],
220
+ > = DurableObjectDefinition.HandlerEffect<ROut, Self, Key>;
@@ -0,0 +1,214 @@
1
+ import { Effect, type Layer } from "effect";
2
+
3
+ import * as DurableObjectEntrypoint from "./DurableObject";
4
+ import type { DurableObjectHandler } from "./DurableObject";
5
+ import * as DurableObjectNamespace from "./DurableObjectNamespace";
6
+ import type { DurableObjectState } from "./DurableObjectState";
7
+ import * as RpcDefinition from "./RpcDefinition";
8
+ import type { WorkerEnvironment } from "./Environment";
9
+
10
+ /**
11
+ * RPC contract for a Durable Object class.
12
+ */
13
+ export type Definition<
14
+ Id extends string = string,
15
+ Methods extends RpcDefinition.Methods = RpcDefinition.Methods,
16
+ > = RpcDefinition.Definition<Id, Methods>;
17
+
18
+ export namespace Definition {
19
+ export type Any = RpcDefinition.Definition.Any;
20
+ }
21
+
22
+ export type ReservedMethodName =
23
+ | RpcDefinition.ReservedMethodName
24
+ | "fetch"
25
+ | "alarm"
26
+ | "webSocketMessage"
27
+ | "webSocketClose"
28
+ | "webSocketError";
29
+
30
+ const reservedMethodNames = new Set<string>([
31
+ "constructor",
32
+ "dup",
33
+ "fetch",
34
+ "alarm",
35
+ "webSocketMessage",
36
+ "webSocketClose",
37
+ "webSocketError",
38
+ ]);
39
+
40
+ /**
41
+ * Promise-based client API derived from a Durable Object definition.
42
+ */
43
+ export type ServerApi<Self extends Definition.Any> = RpcDefinition.Definition.ServerApi<Self>;
44
+
45
+ export type Api<Self extends Definition.Any> = RpcDefinition.Definition.Api<
46
+ Self,
47
+ ReservedMethodName
48
+ >;
49
+
50
+ declare const NamespaceServiceTypeId: unique symbol;
51
+
52
+ /**
53
+ * Nominal service marker for a Durable Object namespace binding.
54
+ */
55
+ export interface NamespaceService<Id extends string, Self extends Definition.Any> {
56
+ readonly [NamespaceServiceTypeId]: {
57
+ readonly id: Id;
58
+ readonly definition: Self;
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Effect handlers for each RPC method in a Durable Object definition.
64
+ */
65
+ export type Handlers<ROut, Self extends Definition.Any> = {
66
+ readonly [Key in keyof Self["methods"]]: (
67
+ ...args: RpcDefinition.Method.Args<Self["methods"][Key]>
68
+ ) => DurableObjectHandler<ROut, RpcDefinition.Method.Success<Self["methods"][Key]>>;
69
+ };
70
+
71
+ type BoundaryHandlers<ROut, Self extends Definition.Any> = {
72
+ readonly [Key in keyof Self["methods"]]: (
73
+ ...args: Array<unknown>
74
+ ) => DurableObjectHandler<ROut, RpcDefinition.Method.EncodedSuccess<Self["methods"][Key]>>;
75
+ };
76
+
77
+ /**
78
+ * Durable Object constructor options for a specific RPC definition.
79
+ */
80
+ export interface Options<ROut, Self extends Definition.Any> extends Omit<
81
+ DurableObjectEntrypoint.DurableObjectOptions<ROut, Handlers<ROut, Self>>,
82
+ "rpc"
83
+ > {
84
+ readonly rpc: Handlers<ROut, Self>;
85
+ }
86
+
87
+ /**
88
+ * Defines a single RPC method schema in a Durable Object definition.
89
+ */
90
+ export const method = RpcDefinition.method;
91
+
92
+ /**
93
+ * Creates a Durable Object RPC definition plus implementation/binding helpers.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const ChatRoom = DurableObjectDefinition.make("ChatRoom", {
98
+ * postMessage: DurableObjectDefinition.method({
99
+ * args: [Schema.String],
100
+ * success: Schema.Void,
101
+ * }),
102
+ * });
103
+ * ```
104
+ */
105
+ const makeDefinition = <Id extends string, const MethodsShape extends RpcDefinition.Methods>(
106
+ id: Id,
107
+ methods: MethodsShape & RpcDefinition.NoReservedMethods<MethodsShape, ReservedMethodName>,
108
+ ) => {
109
+ type SelfDefinition = Definition<Id, MethodsShape>;
110
+ RpcDefinition.assertNoReservedMethods("Durable Object", methods, reservedMethodNames);
111
+ const definition: SelfDefinition = RpcDefinition.make(id, methods);
112
+
113
+ return Object.assign(definition, {
114
+ make: <ROut, LayerError>(
115
+ layer: Layer.Layer<ROut, LayerError, DurableObjectState | WorkerEnvironment>,
116
+ options: Options<ROut, SelfDefinition>,
117
+ ) =>
118
+ DurableObjectEntrypoint.make(layer, {
119
+ ...options,
120
+ rpc: wrapHandlers(definition, options.rpc),
121
+ }),
122
+ Namespace:
123
+ <Self>() =>
124
+ <BindingId extends string>(
125
+ bindingId: BindingId,
126
+ binding: Omit<
127
+ DurableObjectNamespace.DurableObjectNamespaceBindingDefinition<SelfDefinition>,
128
+ "definition"
129
+ >,
130
+ ) =>
131
+ DurableObjectNamespace.Service<Self, Api<SelfDefinition>>()<BindingId, SelfDefinition>(
132
+ bindingId,
133
+ {
134
+ ...binding,
135
+ definition,
136
+ },
137
+ ),
138
+ namespace: <BindingId extends string>(
139
+ bindingId: BindingId,
140
+ binding: Omit<
141
+ DurableObjectNamespace.DurableObjectNamespaceBindingDefinition<SelfDefinition>,
142
+ "definition"
143
+ >,
144
+ ) =>
145
+ DurableObjectNamespace.Service<
146
+ NamespaceService<BindingId, SelfDefinition>,
147
+ Api<SelfDefinition>
148
+ >()<BindingId, SelfDefinition>(bindingId, {
149
+ ...binding,
150
+ definition,
151
+ }),
152
+ });
153
+ };
154
+
155
+ export const make = makeDefinition;
156
+
157
+ export const Tag =
158
+ <_Self>() =>
159
+ <Id extends string, const MethodsShape extends RpcDefinition.Methods>(
160
+ id: Id,
161
+ methods: MethodsShape & RpcDefinition.NoReservedMethods<MethodsShape, ReservedMethodName>,
162
+ ) => {
163
+ const definition = makeDefinition<Id, MethodsShape>(id, methods);
164
+
165
+ abstract class DurableObjectDefinitionClass {
166
+ static readonly id = definition.id;
167
+ static readonly methods = definition.methods;
168
+ static readonly make = definition.make;
169
+ static readonly Namespace = definition.Namespace;
170
+ static readonly namespace = definition.namespace;
171
+ }
172
+
173
+ return DurableObjectDefinitionClass as (abstract new () => object) & typeof definition;
174
+ };
175
+
176
+ export const DurableObject = Tag;
177
+
178
+ const wrapHandlers = <ROut, const Self extends Definition.Any>(
179
+ definition: Self,
180
+ handlers: Handlers<ROut, Self>,
181
+ ): BoundaryHandlers<ROut, Self> => {
182
+ const wrapped = {} as Record<string, unknown>;
183
+
184
+ for (const key of Object.keys(definition.methods) as Array<
185
+ RpcDefinition.Definition.MethodNames<Self>
186
+ >) {
187
+ const handler = handlers[key];
188
+ wrapped[key] = (...args: Array<unknown>) =>
189
+ Effect.gen(function* () {
190
+ const decodedArgs = yield* RpcDefinition.decodeArgs(definition, key, args);
191
+ const value = yield* handler(...decodedArgs);
192
+ return yield* RpcDefinition.encodeSuccess(definition, key, value);
193
+ });
194
+ }
195
+
196
+ return wrapped as BoundaryHandlers<ROut, Self>;
197
+ };
198
+
199
+ /**
200
+ * Helper for implementing handlers with the exact method shape of a definition.
201
+ */
202
+ export const implement = <ROut, const Self extends Definition.Any>(
203
+ _definition: Self,
204
+ handlers: Handlers<ROut, Self>,
205
+ ): Handlers<ROut, Self> => handlers;
206
+
207
+ /**
208
+ * Convenience alias for a single Durable Object RPC handler Effect.
209
+ */
210
+ export type HandlerEffect<
211
+ ROut,
212
+ Self extends Definition.Any,
213
+ Key extends keyof Self["methods"],
214
+ > = DurableObjectHandler<ROut, RpcDefinition.Method.Success<Self["methods"][Key]>>;