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.
@@ -0,0 +1,206 @@
1
+ import { Effect, type Layer } from "effect";
2
+
3
+ import type { WorkerEnvironment } from "./Environment";
4
+ import * as WorkerEntrypoint from "./Worker";
5
+ import type { WorkerRpcHandler } from "./Worker";
6
+ import * as RpcDefinition from "./RpcDefinition";
7
+ import * as ServiceBinding from "./ServiceBinding";
8
+
9
+ /**
10
+ * RPC contract for a Worker service.
11
+ *
12
+ * Create with {@link make} and reuse to type both worker implementations and
13
+ * service bindings in other workers.
14
+ */
15
+ export type Definition<
16
+ Id extends string = string,
17
+ Methods extends RpcDefinition.Methods = RpcDefinition.Methods,
18
+ > = RpcDefinition.Definition<Id, Methods>;
19
+
20
+ export namespace Definition {
21
+ export type Any = RpcDefinition.Definition.Any;
22
+ }
23
+
24
+ export type ReservedMethodName = WorkerEntrypoint.ReservedMethodName;
25
+
26
+ const reservedMethodNames = new Set<string>([
27
+ "constructor",
28
+ "dup",
29
+ "fetch",
30
+ "connect",
31
+ "alarm",
32
+ "webSocketMessage",
33
+ "webSocketClose",
34
+ "webSocketError",
35
+ ]);
36
+
37
+ /**
38
+ * Promise-based client API derived from a {@link Definition}.
39
+ */
40
+ export type ServerApi<Self extends Definition.Any> = RpcDefinition.Definition.ServerApi<Self>;
41
+
42
+ export type Api<Self extends Definition.Any> = RpcDefinition.Definition.Api<
43
+ Self,
44
+ ReservedMethodName
45
+ >;
46
+
47
+ declare const BindingServiceTypeId: unique symbol;
48
+
49
+ /**
50
+ * Nominal service marker for a worker binding created with {@link make}.
51
+ */
52
+ export interface BindingService<Id extends string, Self extends Definition.Any> {
53
+ readonly [BindingServiceTypeId]: {
54
+ readonly id: Id;
55
+ readonly definition: Self;
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Effect handlers for each RPC method in a worker definition.
61
+ */
62
+ export type Handlers<ROut, Self extends Definition.Any> = {
63
+ readonly [Key in keyof Self["methods"]]: (
64
+ ...args: RpcDefinition.Method.Args<Self["methods"][Key]>
65
+ ) => WorkerRpcHandler<ROut, RpcDefinition.Method.Success<Self["methods"][Key]>>;
66
+ };
67
+
68
+ type BoundaryHandlers<ROut, Self extends Definition.Any> = {
69
+ readonly [Key in keyof Self["methods"]]: (
70
+ ...args: Array<unknown>
71
+ ) => WorkerRpcHandler<ROut, RpcDefinition.Method.EncodedSuccess<Self["methods"][Key]>>;
72
+ };
73
+
74
+ /**
75
+ * Worker constructor options for a specific RPC definition.
76
+ */
77
+ export interface Options<ROut, Self extends Definition.Any> extends Omit<
78
+ WorkerEntrypoint.WorkerOptions<ROut, Handlers<ROut, Self>>,
79
+ "rpc"
80
+ > {
81
+ readonly rpc: Handlers<ROut, Self>;
82
+ }
83
+
84
+ /**
85
+ * Defines a single RPC method schema in a worker definition.
86
+ */
87
+ export const method = RpcDefinition.method;
88
+
89
+ /**
90
+ * Creates a typed worker RPC definition plus helpers for implementation and bindings.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const CounterWorker = WorkerDefinition.make("CounterWorker", {
95
+ * increment: WorkerDefinition.method({
96
+ * args: [Schema.Number],
97
+ * success: Schema.Number,
98
+ * }),
99
+ * });
100
+ * ```
101
+ */
102
+ const makeDefinition = <Id extends string, const MethodsShape extends RpcDefinition.Methods>(
103
+ id: Id,
104
+ methods: MethodsShape & RpcDefinition.NoReservedMethods<MethodsShape, ReservedMethodName>,
105
+ ) => {
106
+ type SelfDefinition = Definition<Id, MethodsShape>;
107
+ RpcDefinition.assertNoReservedMethods("Worker", methods, reservedMethodNames);
108
+ const definition: SelfDefinition = RpcDefinition.make(id, methods);
109
+
110
+ return Object.assign(definition, {
111
+ make: <ROut, LayerError>(
112
+ layer: Layer.Layer<
113
+ ROut,
114
+ LayerError,
115
+ WorkerEntrypoint.ExecutionContext | WorkerEntrypoint.WorkerContext | WorkerEnvironment
116
+ >,
117
+ options: Options<ROut, SelfDefinition>,
118
+ ) =>
119
+ WorkerEntrypoint.make(layer, {
120
+ ...options,
121
+ rpc: wrapHandlers(definition, options.rpc),
122
+ }),
123
+ Binding:
124
+ <Self>() =>
125
+ <BindingId extends string>(
126
+ bindingId: BindingId,
127
+ binding: Omit<ServiceBinding.ServiceBindingDefinition<SelfDefinition>, "definition">,
128
+ ) =>
129
+ ServiceBinding.Service<Self, Api<SelfDefinition>>()<BindingId, SelfDefinition>(bindingId, {
130
+ ...binding,
131
+ definition,
132
+ }),
133
+ binding: <BindingId extends string>(
134
+ bindingId: BindingId,
135
+ binding: Omit<ServiceBinding.ServiceBindingDefinition<SelfDefinition>, "definition">,
136
+ ) =>
137
+ ServiceBinding.Service<BindingService<BindingId, SelfDefinition>, Api<SelfDefinition>>()<
138
+ BindingId,
139
+ SelfDefinition
140
+ >(bindingId, {
141
+ ...binding,
142
+ definition,
143
+ }),
144
+ });
145
+ };
146
+
147
+ export const make = makeDefinition;
148
+
149
+ export const Tag =
150
+ <_Self>() =>
151
+ <Id extends string, const MethodsShape extends RpcDefinition.Methods>(
152
+ id: Id,
153
+ methods: MethodsShape & RpcDefinition.NoReservedMethods<MethodsShape, ReservedMethodName>,
154
+ ) => {
155
+ const definition = makeDefinition<Id, MethodsShape>(id, methods);
156
+
157
+ abstract class WorkerDefinitionClass {
158
+ static readonly id = definition.id;
159
+ static readonly methods = definition.methods;
160
+ static readonly make = definition.make;
161
+ static readonly Binding = definition.Binding;
162
+ static readonly binding = definition.binding;
163
+ }
164
+
165
+ return WorkerDefinitionClass as (abstract new () => object) & typeof definition;
166
+ };
167
+
168
+ export const Worker = Tag;
169
+
170
+ const wrapHandlers = <ROut, const Self extends Definition.Any>(
171
+ definition: Self,
172
+ handlers: Handlers<ROut, Self>,
173
+ ): BoundaryHandlers<ROut, Self> => {
174
+ const wrapped = {} as Record<string, unknown>;
175
+
176
+ for (const key of Object.keys(definition.methods) as Array<
177
+ RpcDefinition.Definition.MethodNames<Self>
178
+ >) {
179
+ const handler = handlers[key];
180
+ wrapped[key] = (...args: Array<unknown>) =>
181
+ Effect.gen(function* () {
182
+ const decodedArgs = yield* RpcDefinition.decodeArgs(definition, key, args);
183
+ const value = yield* handler(...decodedArgs);
184
+ return yield* RpcDefinition.encodeSuccess(definition, key, value);
185
+ });
186
+ }
187
+
188
+ return wrapped as BoundaryHandlers<ROut, Self>;
189
+ };
190
+
191
+ /**
192
+ * Helper for implementing handlers with the exact method shape of a definition.
193
+ */
194
+ export const implement = <ROut, const Self extends Definition.Any>(
195
+ _definition: Self,
196
+ handlers: Handlers<ROut, Self>,
197
+ ): Handlers<ROut, Self> => handlers;
198
+
199
+ /**
200
+ * Convenience alias for a single worker RPC handler Effect.
201
+ */
202
+ export type HandlerEffect<
203
+ ROut,
204
+ Self extends Definition.Any,
205
+ Key extends keyof Self["methods"],
206
+ > = WorkerRpcHandler<ROut, RpcDefinition.Method.Success<Self["methods"][Key]>>;
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * as Binding from "./Binding";
2
+ export * as DurableObject from "./DurableObject";
3
+ export * as DurableObjectDefinition from "./DurableObjectDefinition";
4
+ export * as DurableObjectNamespace from "./DurableObjectNamespace";
5
+ export * as DurableObjectRpcWebSocket from "./DurableObjectRpcWebSocket";
6
+ export * as DurableObjectState from "./DurableObjectState";
7
+ export * as DurableObjectWebSocket from "./DurableObjectWebSocket";
8
+ export * as DurableObjectStorage from "./DurableObjectStorage";
9
+ export * as Kv from "./Kv";
10
+ export * as Rpc from "./Rpc";
11
+ export * as RpcDefinition from "./RpcDefinition";
12
+ export * as ServiceBinding from "./ServiceBinding";
13
+ export * as Worker from "./Worker";
14
+ export * as WorkerDefinition from "./WorkerDefinition";
15
+ export { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
@@ -0,0 +1,40 @@
1
+ import { Effect, Layer } from "effect";
2
+
3
+ import * as RpcDefinition from "../RpcDefinition";
4
+
5
+ type AnyArgs = Array<any>;
6
+ type EntrypointRpcMethod = (...args: AnyArgs) => Effect.Effect<any, any, any>;
7
+
8
+ export type EntrypointRpc = Record<string, EntrypointRpcMethod>;
9
+
10
+ export const provideEntrypointServices = <ROut, LayerError, RIn>(
11
+ layer: Layer.Layer<ROut, LayerError, RIn>,
12
+ services: Layer.Layer<RIn, never, never>,
13
+ ): Layer.Layer<ROut | RIn, LayerError, never> =>
14
+ layer.pipe(Layer.provideMerge(services)) as Layer.Layer<ROut | RIn, LayerError, never>;
15
+
16
+ export const defineEntrypointRpcMethods = <Self>(
17
+ target: string,
18
+ prototype: object,
19
+ rpc: EntrypointRpc | undefined,
20
+ reservedMethodNames: ReadonlySet<string>,
21
+ run: (self: Self, effect: Effect.Effect<any, any, any>) => Promise<unknown>,
22
+ ): void => {
23
+ const methods = rpc ?? {};
24
+
25
+ RpcDefinition.assertNoReservedMethods(target, methods, reservedMethodNames);
26
+
27
+ for (const [key, method] of Object.entries(methods)) {
28
+ Object.defineProperty(prototype, key, {
29
+ enumerable: true,
30
+ value(this: Self, ...args: AnyArgs) {
31
+ return run(
32
+ this,
33
+ Effect.suspend(() => method(...args)),
34
+ );
35
+ },
36
+ });
37
+ }
38
+ };
39
+
40
+ export const assumeEntrypointClass = <Class>(entrypoint: unknown): Class => entrypoint as Class;
@@ -0,0 +1,73 @@
1
+ import { Effect } from "effect";
2
+
3
+ import type * as CloudflareRpc from "../Rpc";
4
+
5
+ type AnyArgs = Array<any>;
6
+
7
+ export type AsyncMethodKey<Api> = {
8
+ [Key in keyof Api]-?: Key extends string
9
+ ? Api[Key] extends (...args: AnyArgs) => Promise<unknown>
10
+ ? Key
11
+ : never
12
+ : never;
13
+ }[keyof Api];
14
+
15
+ export type AsyncMethodArgs<Api, Method extends keyof Api> = Api[Method] extends (
16
+ ...args: infer Args
17
+ ) => Promise<unknown>
18
+ ? Args
19
+ : never;
20
+
21
+ export type AsyncMethodSuccess<Api, Method extends keyof Api> = Api[Method] extends (
22
+ ...args: AnyArgs
23
+ ) => Promise<infer A>
24
+ ? A
25
+ : never;
26
+
27
+ export type AsyncMethodCloudflareReturn<Api, Method extends keyof Api> = CloudflareRpc.Result<
28
+ AsyncMethodSuccess<Api, Method>
29
+ >;
30
+
31
+ const isPropertyTarget = (value: unknown): value is object =>
32
+ (typeof value === "object" || typeof value === "function") && value !== null;
33
+
34
+ export const lookupRpcMethod = <Api, Method extends AsyncMethodKey<Api>, Error>(
35
+ target: unknown,
36
+ method: Method,
37
+ makeError: (cause: unknown) => Error,
38
+ ): Effect.Effect<
39
+ (...args: AsyncMethodArgs<Api, Method>) => AsyncMethodCloudflareReturn<Api, Method>,
40
+ Error
41
+ > =>
42
+ Effect.try({
43
+ try: () => {
44
+ if (!isPropertyTarget(target)) {
45
+ throw new TypeError(`RPC target is not object-like`);
46
+ }
47
+
48
+ const value = Reflect.get(target, method);
49
+
50
+ if (typeof value !== "function") {
51
+ throw new TypeError(`RPC method "${String(method)}" is not callable`);
52
+ }
53
+
54
+ return ((...args: AsyncMethodArgs<Api, Method>) => Reflect.apply(value, target, args)) as (
55
+ ...args: AsyncMethodArgs<Api, Method>
56
+ ) => AsyncMethodCloudflareReturn<Api, Method>;
57
+ },
58
+ catch: makeError,
59
+ });
60
+
61
+ export const invokeRpcMethod = <Api, Method extends AsyncMethodKey<Api>, Error>(
62
+ target: unknown,
63
+ method: Method,
64
+ args: AsyncMethodArgs<Api, Method>,
65
+ makeError: (cause: unknown) => Error,
66
+ ): Effect.Effect<AsyncMethodCloudflareReturn<Api, Method>, Error> =>
67
+ Effect.gen(function* () {
68
+ const fn = yield* lookupRpcMethod<Api, Method, Error>(target, method, makeError);
69
+ return yield* Effect.try({
70
+ try: () => fn(...args),
71
+ catch: makeError,
72
+ });
73
+ });