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/src/Rpc.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { RpcStub, RpcTarget } from "cloudflare:workers";
2
+ import { Effect, type Scope } from "effect";
3
+
4
+ export { RpcStub, RpcTarget };
5
+
6
+ export type Stubable = Rpc.Stubable;
7
+
8
+ export type Stub<T extends Stubable> = Rpc.Stub<T>;
9
+
10
+ export type Provider<T extends object, Reserved extends string = never> = Rpc.Provider<T, Reserved>;
11
+
12
+ type BaseType =
13
+ | void
14
+ | undefined
15
+ | null
16
+ | boolean
17
+ | number
18
+ | bigint
19
+ | string
20
+ | TypedArray
21
+ | ArrayBuffer
22
+ | DataView
23
+ | Date
24
+ | Error
25
+ | RegExp
26
+ | ReadableStream<Uint8Array>
27
+ | WritableStream<Uint8Array>
28
+ | Request
29
+ | Response
30
+ | Headers;
31
+
32
+ export type Serializable<T> =
33
+ | BaseType
34
+ | Map<
35
+ T extends Map<infer Key, unknown> ? Serializable<Key> : never,
36
+ T extends Map<unknown, infer Value> ? Serializable<Value> : never
37
+ >
38
+ | Set<T extends Set<infer Value> ? Serializable<Value> : never>
39
+ | ReadonlyArray<T extends ReadonlyArray<infer Value> ? Serializable<Value> : never>
40
+ | {
41
+ [Key in keyof T]: Key extends number | string ? Serializable<T[Key]> : never;
42
+ }
43
+ | Stub<Stubable>
44
+ | Stubable;
45
+
46
+ export type Stubify<T> = T extends Stubable
47
+ ? Stub<T>
48
+ : T extends Map<infer Key, infer Value>
49
+ ? Map<Stubify<Key>, Stubify<Value>>
50
+ : T extends Set<infer Value>
51
+ ? Set<Stubify<Value>>
52
+ : T extends Array<infer Value>
53
+ ? Array<Stubify<Value>>
54
+ : T extends ReadonlyArray<infer Value>
55
+ ? ReadonlyArray<Stubify<Value>>
56
+ : T extends BaseType
57
+ ? T
58
+ : T extends {
59
+ [key: string | number]: unknown;
60
+ }
61
+ ? {
62
+ [Key in keyof T]: Stubify<T[Key]>;
63
+ }
64
+ : T;
65
+
66
+ type MaybeProvider<T> = T extends object ? Provider<T> : unknown;
67
+ type MaybeDisposable<T> = T extends object ? Disposable : unknown;
68
+
69
+ export type Result<T> = T extends Stubable
70
+ ? Promise<Stub<T>> & Provider<T>
71
+ : T extends Serializable<T>
72
+ ? Promise<Stubify<T> & MaybeDisposable<T>> & MaybeProvider<T>
73
+ : never;
74
+
75
+ export type MethodKey<Api> = {
76
+ [Key in keyof Api]-?: Key extends string
77
+ ? Api[Key] extends (...args: Array<any>) => unknown
78
+ ? Key
79
+ : never
80
+ : never;
81
+ }[keyof Api];
82
+
83
+ export type MethodArgs<Api, Method extends keyof Api> = Api[Method] extends (
84
+ ...args: infer Args
85
+ ) => unknown
86
+ ? Args
87
+ : never;
88
+
89
+ export type MethodReturn<Api, Method extends keyof Api> = Api[Method] extends (
90
+ ...args: Array<any>
91
+ ) => infer Return
92
+ ? Return
93
+ : never;
94
+
95
+ export type DisposableValue = {
96
+ [Symbol.dispose](): void;
97
+ };
98
+
99
+ export const isDisposable = (value: unknown): value is DisposableValue =>
100
+ (typeof value === "object" || typeof value === "function") &&
101
+ value !== null &&
102
+ Symbol.dispose in value &&
103
+ typeof (value as { readonly [Symbol.dispose]?: unknown })[Symbol.dispose] === "function";
104
+
105
+ export const dispose = (value: unknown): Effect.Effect<void> =>
106
+ Effect.sync(() => {
107
+ if (isDisposable(value)) {
108
+ value[Symbol.dispose]();
109
+ }
110
+ });
111
+
112
+ export const resolve = <A>(value: A): Effect.Effect<Awaited<A>, unknown> =>
113
+ isPromiseLike(value)
114
+ ? Effect.tryPromise({
115
+ try: () => value,
116
+ catch: (cause) => cause,
117
+ })
118
+ : Effect.sync(() => value as Awaited<A>);
119
+
120
+ export const scoped = <A>(value: A): Effect.Effect<Awaited<A>, unknown, Scope.Scope> =>
121
+ Effect.acquireRelease(resolve(value), (resolved) => dispose(resolved));
122
+
123
+ const isPromiseLike = <A>(value: A): value is A & PromiseLike<Awaited<A>> =>
124
+ (typeof value === "object" || typeof value === "function") &&
125
+ value !== null &&
126
+ "then" in value &&
127
+ typeof (value as { readonly then?: unknown }).then === "function";
@@ -0,0 +1,354 @@
1
+ import { Data, Effect, Schema as S } from "effect";
2
+
3
+ import type * as Rpc from "./Rpc";
4
+
5
+ export class RpcReservedMethodNameError extends Data.TaggedError("RpcReservedMethodNameError")<{
6
+ readonly definition: string;
7
+ readonly method: string;
8
+ }> {}
9
+
10
+ export class RpcArgumentCountError extends Data.TaggedError("RpcArgumentCountError")<{
11
+ readonly definition: string;
12
+ readonly method: string;
13
+ readonly expected: number;
14
+ readonly actual: number;
15
+ }> {}
16
+
17
+ export class RpcArgumentDecodeError extends Data.TaggedError("RpcArgumentDecodeError")<{
18
+ readonly definition: string;
19
+ readonly method: string;
20
+ readonly index: number;
21
+ readonly cause: unknown;
22
+ }> {}
23
+
24
+ export class RpcArgumentEncodeError extends Data.TaggedError("RpcArgumentEncodeError")<{
25
+ readonly definition: string;
26
+ readonly method: string;
27
+ readonly index: number;
28
+ readonly cause: unknown;
29
+ }> {}
30
+
31
+ export class RpcSuccessDecodeError extends Data.TaggedError("RpcSuccessDecodeError")<{
32
+ readonly definition: string;
33
+ readonly method: string;
34
+ readonly cause: unknown;
35
+ }> {}
36
+
37
+ export class RpcSuccessEncodeError extends Data.TaggedError("RpcSuccessEncodeError")<{
38
+ readonly definition: string;
39
+ readonly method: string;
40
+ readonly cause: unknown;
41
+ }> {}
42
+
43
+ export const reservedMethodNames = new Set([
44
+ "constructor",
45
+ "fetch",
46
+ "connect",
47
+ "alarm",
48
+ "webSocketMessage",
49
+ "webSocketClose",
50
+ "webSocketError",
51
+ "then",
52
+ "dup",
53
+ "dispose",
54
+ "serialize",
55
+ "deserialize",
56
+ ]);
57
+
58
+ export type ReservedMethodName =
59
+ | "constructor"
60
+ | "fetch"
61
+ | "connect"
62
+ | "alarm"
63
+ | "webSocketMessage"
64
+ | "webSocketClose"
65
+ | "webSocketError"
66
+ | "then"
67
+ | "dup"
68
+ | "dispose"
69
+ | "serialize"
70
+ | "deserialize";
71
+
72
+ export type ServiceFreeSchema = S.Codec<any, any, never, never>;
73
+
74
+ export interface Method<
75
+ Args extends ReadonlyArray<ServiceFreeSchema> = ReadonlyArray<ServiceFreeSchema>,
76
+ Success extends ServiceFreeSchema = ServiceFreeSchema,
77
+ > {
78
+ readonly args: Args;
79
+ readonly success: Success;
80
+ }
81
+
82
+ export namespace Method {
83
+ export type Any = Method<ReadonlyArray<ServiceFreeSchema>, ServiceFreeSchema>;
84
+
85
+ type ArgsFromSchemas<Args extends ReadonlyArray<ServiceFreeSchema>> = Args extends readonly []
86
+ ? []
87
+ : Args extends readonly [
88
+ infer Head extends ServiceFreeSchema,
89
+ ...infer Tail extends ReadonlyArray<ServiceFreeSchema>,
90
+ ]
91
+ ? [S.Schema.Type<Head>, ...ArgsFromSchemas<Tail>]
92
+ : Array<S.Schema.Type<Args[number]>>;
93
+
94
+ type EncodedArgsFromSchemas<Args extends ReadonlyArray<ServiceFreeSchema>> =
95
+ Args extends readonly []
96
+ ? []
97
+ : Args extends readonly [
98
+ infer Head extends ServiceFreeSchema,
99
+ ...infer Tail extends ReadonlyArray<ServiceFreeSchema>,
100
+ ]
101
+ ? [S.Codec.Encoded<Head>, ...EncodedArgsFromSchemas<Tail>]
102
+ : Array<S.Codec.Encoded<Args[number]>>;
103
+
104
+ export type Args<Self extends Any> = ArgsFromSchemas<Self["args"]>;
105
+
106
+ export type EncodedArgs<Self extends Any> = EncodedArgsFromSchemas<Self["args"]>;
107
+
108
+ export type Success<Self extends Any> = S.Schema.Type<Self["success"]>;
109
+
110
+ export type EncodedSuccess<Self extends Any> = S.Codec.Encoded<Self["success"]>;
111
+ }
112
+
113
+ export type Methods = Record<string, Method.Any>;
114
+
115
+ export type NoReservedMethods<
116
+ MethodsShape extends Methods,
117
+ Reserved extends string = ReservedMethodName,
118
+ > = Extract<keyof MethodsShape, Reserved> extends never ? MethodsShape : never;
119
+
120
+ export interface Definition<Id extends string = string, MethodsShape extends Methods = Methods> {
121
+ readonly id: Id;
122
+ readonly methods: MethodsShape;
123
+ }
124
+
125
+ export namespace Definition {
126
+ export type Any = Definition<string, Methods>;
127
+
128
+ export type ServerApi<Self extends Any> = {
129
+ readonly [Key in keyof Self["methods"]]: (
130
+ ...args: Method.Args<Self["methods"][Key]>
131
+ ) => Promise<Method.Success<Self["methods"][Key]>>;
132
+ };
133
+
134
+ export type Api<Self extends Any, Reserved extends string = never> = Rpc.Provider<
135
+ ServerApi<Self>,
136
+ Reserved
137
+ >;
138
+
139
+ export type MethodNames<Self extends Any> = Extract<keyof Self["methods"], string>;
140
+ }
141
+
142
+ export class ReservedMethodNameError extends Error {
143
+ readonly method: string;
144
+ readonly target: string;
145
+
146
+ constructor(target: string, method: string) {
147
+ super(`${target} RPC method "${method}" is reserved by Cloudflare Workers RPC`);
148
+ this.name = "ReservedMethodNameError";
149
+ this.target = target;
150
+ this.method = method;
151
+ }
152
+ }
153
+
154
+ export const assertNoReservedMethods = (
155
+ target: string,
156
+ methods: Record<string, unknown>,
157
+ reserved: ReadonlySet<string>,
158
+ ) => {
159
+ for (const method of Object.keys(methods)) {
160
+ if (reserved.has(method)) {
161
+ throw new ReservedMethodNameError(target, method);
162
+ }
163
+ }
164
+ };
165
+
166
+ export function method<Success extends ServiceFreeSchema>(definition: {
167
+ readonly success: Success;
168
+ }): Method<readonly [], Success>;
169
+ export function method<
170
+ const Args extends ReadonlyArray<ServiceFreeSchema>,
171
+ Success extends ServiceFreeSchema,
172
+ >(definition: { readonly args: Args; readonly success: Success }): Method<Args, Success>;
173
+ export function method(definition: {
174
+ readonly args?: ReadonlyArray<ServiceFreeSchema>;
175
+ readonly success: ServiceFreeSchema;
176
+ }) {
177
+ return {
178
+ args: definition.args ?? [],
179
+ success: definition.success,
180
+ };
181
+ }
182
+
183
+ export const assertNoReservedMethodNames = (
184
+ definition: Definition.Any,
185
+ ): Effect.Effect<void, RpcReservedMethodNameError> =>
186
+ Effect.forEach(Object.keys(definition.methods), (method) =>
187
+ reservedMethodNames.has(method)
188
+ ? Effect.fail(new RpcReservedMethodNameError({ definition: definition.id, method }))
189
+ : Effect.void,
190
+ ).pipe(Effect.asVoid);
191
+
192
+ export const decodeArgs = <
193
+ const Self extends Definition.Any,
194
+ MethodName extends Definition.MethodNames<Self>,
195
+ >(
196
+ definition: Self,
197
+ methodName: MethodName,
198
+ args: ReadonlyArray<unknown>,
199
+ ): Effect.Effect<
200
+ Method.Args<Self["methods"][MethodName]>,
201
+ RpcArgumentCountError | RpcArgumentDecodeError
202
+ > =>
203
+ Effect.gen(function* () {
204
+ const methodDefinition = definition.methods[methodName];
205
+
206
+ if (args.length !== methodDefinition.args.length) {
207
+ return yield* Effect.fail(
208
+ new RpcArgumentCountError({
209
+ definition: definition.id,
210
+ method: methodName,
211
+ expected: methodDefinition.args.length,
212
+ actual: args.length,
213
+ }),
214
+ );
215
+ }
216
+
217
+ const decoded: Array<unknown> = [];
218
+
219
+ for (let index = 0; index < methodDefinition.args.length; index++) {
220
+ const schema = methodDefinition.args[index];
221
+ decoded.push(
222
+ yield* (S.decodeUnknownEffect(schema)(args[index]) as Effect.Effect<unknown, unknown>).pipe(
223
+ Effect.mapError(
224
+ (cause) =>
225
+ new RpcArgumentDecodeError({
226
+ definition: definition.id,
227
+ method: methodName,
228
+ index,
229
+ cause,
230
+ }),
231
+ ),
232
+ ),
233
+ );
234
+ }
235
+
236
+ return decoded as Method.Args<Self["methods"][MethodName]>;
237
+ });
238
+
239
+ export const encodeArgs = <
240
+ const Self extends Definition.Any,
241
+ MethodName extends Definition.MethodNames<Self>,
242
+ >(
243
+ definition: Self,
244
+ methodName: MethodName,
245
+ args: Method.Args<Self["methods"][MethodName]>,
246
+ ): Effect.Effect<
247
+ Method.EncodedArgs<Self["methods"][MethodName]>,
248
+ RpcArgumentCountError | RpcArgumentEncodeError
249
+ > =>
250
+ Effect.gen(function* () {
251
+ const methodDefinition = definition.methods[methodName];
252
+
253
+ if (args.length !== methodDefinition.args.length) {
254
+ return yield* Effect.fail(
255
+ new RpcArgumentCountError({
256
+ definition: definition.id,
257
+ method: methodName,
258
+ expected: methodDefinition.args.length,
259
+ actual: args.length,
260
+ }),
261
+ );
262
+ }
263
+
264
+ const encoded: Array<unknown> = [];
265
+
266
+ for (let index = 0; index < methodDefinition.args.length; index++) {
267
+ const schema = methodDefinition.args[index];
268
+ encoded.push(
269
+ yield* (S.encodeEffect(schema)(args[index]) as Effect.Effect<unknown, unknown>).pipe(
270
+ Effect.mapError(
271
+ (cause) =>
272
+ new RpcArgumentEncodeError({
273
+ definition: definition.id,
274
+ method: methodName,
275
+ index,
276
+ cause,
277
+ }),
278
+ ),
279
+ ),
280
+ );
281
+ }
282
+
283
+ return encoded as Method.EncodedArgs<Self["methods"][MethodName]>;
284
+ });
285
+
286
+ export const encodeSuccess = <
287
+ const Self extends Definition.Any,
288
+ MethodName extends Definition.MethodNames<Self>,
289
+ >(
290
+ definition: Self,
291
+ methodName: MethodName,
292
+ value: Method.Success<Self["methods"][MethodName]>,
293
+ ): Effect.Effect<Method.EncodedSuccess<Self["methods"][MethodName]>, RpcSuccessEncodeError> => {
294
+ const methodDefinition = definition.methods[methodName];
295
+
296
+ return (
297
+ S.encodeEffect(methodDefinition.success)(value) as Effect.Effect<
298
+ Method.EncodedSuccess<Self["methods"][MethodName]>,
299
+ unknown
300
+ >
301
+ ).pipe(
302
+ Effect.mapError(
303
+ (cause) =>
304
+ new RpcSuccessEncodeError({
305
+ definition: definition.id,
306
+ method: methodName,
307
+ cause,
308
+ }),
309
+ ),
310
+ );
311
+ };
312
+
313
+ export const decodeSuccess = <
314
+ const Self extends Definition.Any,
315
+ MethodName extends Definition.MethodNames<Self>,
316
+ >(
317
+ definition: Self,
318
+ methodName: MethodName,
319
+ value: unknown,
320
+ ): Effect.Effect<Method.Success<Self["methods"][MethodName]>, RpcSuccessDecodeError> => {
321
+ const methodDefinition = definition.methods[methodName];
322
+
323
+ return (
324
+ S.decodeUnknownEffect(methodDefinition.success)(value) as Effect.Effect<
325
+ Method.Success<Self["methods"][MethodName]>,
326
+ unknown
327
+ >
328
+ ).pipe(
329
+ Effect.mapError(
330
+ (cause) =>
331
+ new RpcSuccessDecodeError({
332
+ definition: definition.id,
333
+ method: methodName,
334
+ cause,
335
+ }),
336
+ ),
337
+ );
338
+ };
339
+
340
+ export const make = <Id extends string, const MethodsShape extends Methods>(
341
+ id: Id,
342
+ methods: MethodsShape,
343
+ ): Definition<Id, MethodsShape> => {
344
+ const definition = { id, methods } as Definition<Id, MethodsShape>;
345
+ const reserved = Object.keys(definition.methods).find((methodName) =>
346
+ reservedMethodNames.has(methodName),
347
+ );
348
+
349
+ if (reserved !== undefined) {
350
+ throw new RpcReservedMethodNameError({ definition: id, method: reserved });
351
+ }
352
+
353
+ return definition;
354
+ };