effect-cf 0.6.0 → 0.8.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.
@@ -1,5 +1,6 @@
1
- import { Effect, Schema as S, type Layer, type Scope } from "effect";
1
+ import { Context, Effect, Schema as S, type Layer, type Scope } from "effect";
2
2
 
3
+ import * as Binding from "./Binding";
3
4
  import type { WorkerEnvironment } from "./Environment";
4
5
  import * as QueueEntrypoint from "./Queue";
5
6
  import * as QueueBinding from "./QueueBinding";
@@ -19,15 +20,6 @@ export namespace Definition {
19
20
  export type Any = Definition<string, RpcDefinition.ServiceFreeSchema>;
20
21
  }
21
22
 
22
- declare const BindingServiceTypeId: unique symbol;
23
-
24
- export interface BindingService<Id extends string, Self extends Definition.Any> {
25
- readonly [BindingServiceTypeId]: {
26
- readonly id: Id;
27
- readonly definition: Self;
28
- };
29
- }
30
-
31
23
  export type Handler<ROut, Self extends Definition.Any> = (
32
24
  batch: QueueEntrypoint.QueueBatch<S.Schema.Type<Self["message"]>>,
33
25
  ) => Effect.Effect<
@@ -44,6 +36,44 @@ export interface Options<ROut, Self extends Definition.Any> extends Omit<
44
36
  readonly rpc?: never;
45
37
  }
46
38
 
39
+ export type LayerOptions = {
40
+ readonly binding: string;
41
+ };
42
+
43
+ export interface TagClass<
44
+ Self,
45
+ Id extends string,
46
+ Message extends RpcDefinition.ServiceFreeSchema,
47
+ > extends Context.ServiceClass<Self, Id, QueueBinding.QueueBindingClient<Message>> {
48
+ readonly id: Id;
49
+ readonly message: Message;
50
+ readonly make: <ROut, LayerError>(
51
+ layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
52
+ options: Options<ROut, Definition<Id, Message>>,
53
+ ) => WorkerEntrypoint.WorkerClass<Record<never, never>, ROut>;
54
+ readonly layer: (
55
+ options: LayerOptions,
56
+ ) => Layer.Layer<
57
+ Self,
58
+ Binding.BindingNotFoundError | Binding.BindingValidationError,
59
+ WorkerEnvironment
60
+ >;
61
+ readonly send: (
62
+ message: S.Schema.Type<Message>,
63
+ options?: QueueBinding.QueueSendOptions,
64
+ ) => Effect.Effect<void, QueueBinding.QueueOperationError | S.SchemaError, Self>;
65
+ readonly sendBatch: (
66
+ messages: Iterable<QueueBinding.MessageSendRequest<S.Schema.Type<Message>>>,
67
+ options?: QueueBinding.QueueSendBatchOptions,
68
+ ) => Effect.Effect<void, QueueBinding.QueueOperationError | S.SchemaError, Self>;
69
+ readonly metrics: () => Effect.Effect<
70
+ QueueBinding.QueueMetrics,
71
+ QueueBinding.QueueOperationError,
72
+ Self
73
+ >;
74
+ readonly unsafeRaw: () => Effect.Effect<globalThis.Queue<S.Codec.Encoded<Message>>, never, Self>;
75
+ }
76
+
47
77
  const makeDefinition = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
48
78
  id: Id,
49
79
  definition: { readonly message: Message },
@@ -63,49 +93,65 @@ const makeDefinition = <Id extends string, Message extends RpcDefinition.Service
63
93
  ...options,
64
94
  queue: wrapHandler(queueDefinition, options.queue),
65
95
  }),
66
- Binding:
67
- <Self>() =>
68
- <BindingId extends string>(
69
- bindingId: BindingId,
70
- binding: Omit<QueueBinding.QueueBindingDefinition<Message>, "message">,
71
- ) =>
72
- QueueBinding.Service<Self>()<BindingId, Message>(bindingId, {
73
- ...binding,
74
- message: definition.message,
75
- }),
76
- binding: <BindingId extends string>(
77
- bindingId: BindingId,
78
- binding: Omit<QueueBinding.QueueBindingDefinition<Message>, "message">,
79
- ) =>
80
- QueueBinding.Service<BindingService<BindingId, SelfDefinition>>()<BindingId, Message>(
81
- bindingId,
82
- {
83
- ...binding,
84
- message: definition.message,
85
- },
86
- ),
87
96
  });
88
97
  };
89
98
 
90
- export const make = makeDefinition;
99
+ export const make = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
100
+ id: Id,
101
+ definition: { readonly message: Message },
102
+ ) => Tag<Definition<Id, Message>>()(id, definition);
91
103
 
92
104
  export const Tag =
93
- <_Self>() =>
105
+ <Self>() =>
94
106
  <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
95
107
  id: Id,
96
108
  definition: { readonly message: Message },
97
109
  ) => {
98
110
  const queueDefinition = makeDefinition(id, definition);
111
+ const tag = Context.Service<Self, QueueBinding.QueueBindingClient<Message>>()(id);
112
+
113
+ const layer = (binding: LayerOptions) =>
114
+ QueueBinding.layer(tag, {
115
+ ...binding,
116
+ message: definition.message,
117
+ });
118
+
119
+ const send = Effect.fnUntraced(function* (
120
+ message: S.Schema.Type<Message>,
121
+ options?: QueueBinding.QueueSendOptions,
122
+ ) {
123
+ const queue = yield* tag;
124
+ yield* queue.send(message, options);
125
+ });
99
126
 
100
- abstract class QueueDefinitionClass {
101
- static readonly id = queueDefinition.id;
102
- static readonly message = queueDefinition.message;
103
- static readonly make = queueDefinition.make;
104
- static readonly Binding = queueDefinition.Binding;
105
- static readonly binding = queueDefinition.binding;
106
- }
127
+ const sendBatch = Effect.fnUntraced(function* (
128
+ messages: Iterable<QueueBinding.MessageSendRequest<S.Schema.Type<Message>>>,
129
+ options?: QueueBinding.QueueSendBatchOptions,
130
+ ) {
131
+ const queue = yield* tag;
132
+ yield* queue.sendBatch(messages, options);
133
+ });
134
+
135
+ const metrics = Effect.fnUntraced(function* () {
136
+ const queue = yield* tag;
137
+ return yield* queue.metrics();
138
+ });
139
+
140
+ const unsafeRaw = Effect.fnUntraced(function* () {
141
+ const queue = yield* tag;
142
+ return yield* queue.unsafeRaw;
143
+ });
107
144
 
108
- return QueueDefinitionClass as (abstract new () => object) & typeof queueDefinition;
145
+ return Object.assign(tag, {
146
+ id: queueDefinition.id,
147
+ message: queueDefinition.message,
148
+ make: queueDefinition.make,
149
+ layer,
150
+ send,
151
+ sendBatch,
152
+ metrics,
153
+ unsafeRaw,
154
+ }) as TagClass<Self, Id, Message>;
109
155
  };
110
156
 
111
157
  export const Queue = Tag;
package/src/R2.ts ADDED
@@ -0,0 +1,230 @@
1
+ import { Context, Data, Effect, Option, type Layer } from "effect";
2
+
3
+ import * as Binding from "./Binding";
4
+ import type { WorkerEnvironment } from "./Environment";
5
+
6
+ /** Error raised when an R2 operation fails. */
7
+ export class R2OperationError extends Data.TaggedError("R2OperationError")<{
8
+ readonly binding: string;
9
+ readonly operation: string;
10
+ readonly cause: unknown;
11
+ }> {}
12
+
13
+ /** Typed R2 bucket binding definition. */
14
+ export interface R2Definition {
15
+ /** Binding name as configured in `wrangler.jsonc`. */
16
+ readonly binding: string;
17
+ }
18
+
19
+ export type R2GetOptions = globalThis.R2GetOptions;
20
+ export type R2PutOptions = globalThis.R2PutOptions;
21
+ export type R2ListOptions = globalThis.R2ListOptions;
22
+ export type R2MultipartOptions = globalThis.R2MultipartOptions;
23
+ export type R2UploadPartOptions = globalThis.R2UploadPartOptions;
24
+ export type R2PutValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob;
25
+ export type R2UploadPartValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob;
26
+
27
+ export interface R2MultipartUploadClient {
28
+ readonly raw: globalThis.R2MultipartUpload;
29
+ readonly key: string;
30
+ readonly uploadId: string;
31
+ readonly uploadPart: (
32
+ partNumber: number,
33
+ value: R2UploadPartValue,
34
+ options?: R2UploadPartOptions,
35
+ ) => Effect.Effect<globalThis.R2UploadedPart, R2OperationError>;
36
+ readonly abort: Effect.Effect<void, R2OperationError>;
37
+ readonly complete: (
38
+ uploadedParts: ReadonlyArray<globalThis.R2UploadedPart>,
39
+ ) => Effect.Effect<globalThis.R2Object, R2OperationError>;
40
+ }
41
+
42
+ export interface R2Client {
43
+ readonly head: (
44
+ key: string,
45
+ ) => Effect.Effect<Option.Option<globalThis.R2Object>, R2OperationError>;
46
+ readonly get: {
47
+ (
48
+ key: string,
49
+ options: R2GetOptions & { readonly onlyIf: globalThis.R2Conditional | Headers },
50
+ ): Effect.Effect<
51
+ Option.Option<globalThis.R2ObjectBody | globalThis.R2Object>,
52
+ R2OperationError
53
+ >;
54
+ (
55
+ key: string,
56
+ options?: R2GetOptions,
57
+ ): Effect.Effect<Option.Option<globalThis.R2ObjectBody>, R2OperationError>;
58
+ };
59
+ readonly put: {
60
+ (
61
+ key: string,
62
+ value: R2PutValue,
63
+ options: R2PutOptions & { readonly onlyIf: globalThis.R2Conditional | Headers },
64
+ ): Effect.Effect<Option.Option<globalThis.R2Object>, R2OperationError>;
65
+ (
66
+ key: string,
67
+ value: R2PutValue,
68
+ options?: R2PutOptions,
69
+ ): Effect.Effect<globalThis.R2Object, R2OperationError>;
70
+ };
71
+ readonly createMultipartUpload: (
72
+ key: string,
73
+ options?: R2MultipartOptions,
74
+ ) => Effect.Effect<R2MultipartUploadClient, R2OperationError>;
75
+ readonly resumeMultipartUpload: (
76
+ key: string,
77
+ uploadId: string,
78
+ ) => Effect.Effect<R2MultipartUploadClient, R2OperationError>;
79
+ readonly delete: (keys: string | ReadonlyArray<string>) => Effect.Effect<void, R2OperationError>;
80
+ readonly list: (options?: R2ListOptions) => Effect.Effect<globalThis.R2Objects, R2OperationError>;
81
+ readonly unsafeRaw: Effect.Effect<globalThis.R2Bucket>;
82
+ readonly definition: R2Definition;
83
+ }
84
+
85
+ declare const R2ServiceTypeId: unique symbol;
86
+
87
+ /** Nominal service marker for R2 services created with {@link make}. */
88
+ export interface R2Service<Id extends string> {
89
+ readonly [R2ServiceTypeId]: {
90
+ readonly id: Id;
91
+ };
92
+ }
93
+
94
+ export type LayerOptions = {
95
+ readonly binding: string;
96
+ };
97
+
98
+ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
99
+ Self,
100
+ Id,
101
+ R2Client
102
+ > {
103
+ readonly id: Id;
104
+ readonly layer: (
105
+ options: LayerOptions,
106
+ ) => Layer.Layer<
107
+ Self,
108
+ Binding.BindingNotFoundError | Binding.BindingValidationError,
109
+ WorkerEnvironment
110
+ >;
111
+ }
112
+
113
+ const r2Error = (binding: string, operation: string, cause: unknown) =>
114
+ new R2OperationError({ binding, operation, cause });
115
+
116
+ const tryR2Promise = <A>(
117
+ binding: string,
118
+ operation: string,
119
+ evaluate: () => Promise<A>,
120
+ ): Effect.Effect<A, R2OperationError> =>
121
+ Effect.tryPromise({
122
+ try: evaluate,
123
+ catch: (cause) => r2Error(binding, operation, cause),
124
+ });
125
+
126
+ const tryR2Sync = <A>(
127
+ binding: string,
128
+ operation: string,
129
+ evaluate: () => A,
130
+ ): Effect.Effect<A, R2OperationError> =>
131
+ Effect.try({
132
+ try: evaluate,
133
+ catch: (cause) => r2Error(binding, operation, cause),
134
+ });
135
+
136
+ const maybe = <A>(value: A | null): Option.Option<A> =>
137
+ value === null ? Option.none() : Option.some(value);
138
+
139
+ export const isR2Bucket = (value: unknown): value is globalThis.R2Bucket => {
140
+ if (typeof value !== "object" || value === null) {
141
+ return false;
142
+ }
143
+
144
+ const resource = value as Record<string, unknown>;
145
+
146
+ return (
147
+ typeof resource.head === "function" &&
148
+ typeof resource.get === "function" &&
149
+ typeof resource.put === "function" &&
150
+ typeof resource.createMultipartUpload === "function" &&
151
+ typeof resource.resumeMultipartUpload === "function" &&
152
+ typeof resource.delete === "function" &&
153
+ typeof resource.list === "function"
154
+ );
155
+ };
156
+
157
+ export const makeClient = (
158
+ definition: R2Definition,
159
+ ): ((bucket: globalThis.R2Bucket) => R2Client) => {
160
+ const wrapUpload = (upload: globalThis.R2MultipartUpload): R2MultipartUploadClient => ({
161
+ raw: upload,
162
+ key: upload.key,
163
+ uploadId: upload.uploadId,
164
+ uploadPart: (partNumber, value, options) =>
165
+ tryR2Promise(definition.binding, "uploadPart", () =>
166
+ upload.uploadPart(partNumber, value, options),
167
+ ),
168
+ abort: tryR2Promise(definition.binding, "abortMultipartUpload", () => upload.abort()),
169
+ complete: (uploadedParts) =>
170
+ tryR2Promise(definition.binding, "completeMultipartUpload", () =>
171
+ upload.complete([...uploadedParts]),
172
+ ),
173
+ });
174
+
175
+ return (bucket) => ({
176
+ definition,
177
+ head: (key) =>
178
+ tryR2Promise(definition.binding, "head", () => bucket.head(key)).pipe(Effect.map(maybe)),
179
+ get: ((key: string, options?: R2GetOptions) =>
180
+ tryR2Promise(definition.binding, "get", () => bucket.get(key, options)).pipe(
181
+ Effect.map(maybe),
182
+ )) as R2Client["get"],
183
+ put: ((key: string, value: R2PutValue, options?: R2PutOptions) =>
184
+ tryR2Promise(definition.binding, "put", () => bucket.put(key, value, options)).pipe(
185
+ Effect.map((object) => {
186
+ if (object === null) {
187
+ return Option.none<globalThis.R2Object>();
188
+ }
189
+
190
+ if (options !== undefined && "onlyIf" in options) {
191
+ return Option.some(object);
192
+ }
193
+
194
+ return object;
195
+ }),
196
+ )) as R2Client["put"],
197
+ createMultipartUpload: (key, options) =>
198
+ tryR2Promise(definition.binding, "createMultipartUpload", () =>
199
+ bucket.createMultipartUpload(key, options),
200
+ ).pipe(Effect.map(wrapUpload)),
201
+ resumeMultipartUpload: (key, uploadId) =>
202
+ tryR2Sync(definition.binding, "resumeMultipartUpload", () =>
203
+ wrapUpload(bucket.resumeMultipartUpload(key, uploadId)),
204
+ ),
205
+ delete: (keys) => {
206
+ const nativeKeys = typeof keys === "string" ? keys : [...keys];
207
+ return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
208
+ },
209
+ list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
210
+ unsafeRaw: Effect.succeed(bucket),
211
+ });
212
+ };
213
+
214
+ export const layer = <Self>(tag: Context.Service<Self, R2Client>, definition: R2Definition) =>
215
+ Binding.layer(tag, definition.binding, isR2Bucket, makeClient(definition));
216
+
217
+ export const make = <Id extends string>(id: Id) => Tag<R2Service<Id>>()<Id>(id);
218
+
219
+ export const Tag =
220
+ <Self>() =>
221
+ <Id extends string>(id: Id) => {
222
+ const tag = Context.Service<Self, R2Client>()(id);
223
+
224
+ const makeLayer = (definition: LayerOptions) => layer(tag, definition);
225
+
226
+ return Object.assign(tag, {
227
+ id,
228
+ layer: makeLayer,
229
+ }) as TagClass<Self, Id>;
230
+ };