effect-cf 0.20.1 → 0.23.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +27 -3
  3. package/dist/{Hyperdrive-RTjgirMV.d.mts → Hyperdrive-COyg7R26.d.mts} +40 -13
  4. package/dist/HyperdrivePg.d.mts +13 -5
  5. package/dist/HyperdrivePg.mjs +12 -7
  6. package/dist/index.d.mts +1113 -922
  7. package/dist/index.mjs +980 -604
  8. package/package.json +10 -5
  9. package/src/AiGateway.ts +82 -47
  10. package/src/AnalyticsEngine.ts +74 -58
  11. package/src/Binding.ts +66 -13
  12. package/src/BrowserRendering.ts +30 -7
  13. package/src/Cache.ts +32 -8
  14. package/src/CloudflareOtlp.ts +32 -9
  15. package/src/ContainerNamespace.ts +251 -80
  16. package/src/D1.ts +13 -3
  17. package/src/DurableObject.ts +27 -164
  18. package/src/DurableObjectAlarm.ts +30 -9
  19. package/src/DurableObjectDefinition.ts +58 -54
  20. package/src/DurableObjectNamespace.ts +99 -96
  21. package/src/DurableObjectRpcWebSocket.ts +14 -6
  22. package/src/DurableObjectSqlite.ts +6 -3
  23. package/src/DurableObjectState.ts +63 -39
  24. package/src/DurableObjectStorage.ts +7 -1
  25. package/src/DurableObjectWebSocket.ts +51 -41
  26. package/src/Email.ts +40 -13
  27. package/src/Environment.ts +26 -6
  28. package/src/Hyperdrive.ts +2 -2
  29. package/src/HyperdrivePg.ts +25 -13
  30. package/src/Images.ts +77 -82
  31. package/src/Kv.ts +37 -27
  32. package/src/Queue.ts +6 -1
  33. package/src/QueueBinding.ts +18 -21
  34. package/src/QueueDefinition.ts +15 -14
  35. package/src/R2.ts +107 -29
  36. package/src/Rpc.ts +3 -1
  37. package/src/RpcDefinition.ts +177 -133
  38. package/src/ServiceBinding.ts +112 -108
  39. package/src/Vectorize.ts +59 -14
  40. package/src/Worker.ts +172 -192
  41. package/src/WorkerDefinition.ts +5 -10
  42. package/src/WorkersAi.ts +27 -17
  43. package/src/Workflow.ts +54 -49
  44. package/src/WorkflowBinding.ts +19 -11
  45. package/src/WorkflowDefinition.ts +24 -27
  46. package/src/index.ts +4 -3
  47. package/src/internal/Entrypoint.ts +3 -1
  48. package/src/internal/ErrorMessage.ts +24 -0
  49. package/src/internal/RpcInvocation.ts +11 -8
  50. package/src/internal/Runtime.ts +52 -0
  51. package/src/internal/WorkerContext.ts +39 -17
  52. package/src/Entry.ts +0 -259
  53. package/src/internal/Clock.ts +0 -37
@@ -41,7 +41,7 @@ export interface QueueBindingClient<Message extends RpcDefinition.ServiceFreeSch
41
41
  options?: QueueSendBatchOptions,
42
42
  ) => Effect.Effect<void, QueueOperationError | S.SchemaError>;
43
43
  readonly metrics: () => Effect.Effect<QueueMetrics, QueueOperationError>;
44
- readonly unsafeRaw: Effect.Effect<CloudflareQueue<S.Codec.Encoded<Message>>>;
44
+ readonly rawUnsafe: Effect.Effect<CloudflareQueue<S.Codec.Encoded<Message>>>;
45
45
  }
46
46
 
47
47
  export class QueueOperationError extends Data.TaggedError("QueueOperationError")<{
@@ -86,15 +86,16 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
86
86
  const encodeMessage = S.encodeEffect(definition.message);
87
87
 
88
88
  return (queue) => ({
89
- send: Effect.fnUntraced(function* (message: Body, options?: QueueSendOptions) {
89
+ send: Effect.fn("QueueBinding.send", {
90
+ attributes: { binding: definition.binding, operation: "send" },
91
+ })(function* (message: Body, options?: QueueSendOptions) {
90
92
  const encoded = yield* encodeMessage(message);
91
93
 
92
94
  yield* tryQueuePromise(definition.binding, "send", () => queue.send(encoded, options));
93
95
  }),
94
- sendBatch: Effect.fnUntraced(function* (
95
- messages: Iterable<MessageSendRequest<Body>>,
96
- options?: QueueSendBatchOptions,
97
- ) {
96
+ sendBatch: Effect.fn("QueueBinding.sendBatch", {
97
+ attributes: { binding: definition.binding, operation: "sendBatch" },
98
+ })(function* (messages: Iterable<MessageSendRequest<Body>>, options?: QueueSendBatchOptions) {
98
99
  const encodedMessages: Array<MessageSendRequest<EncodedBody>> = [];
99
100
 
100
101
  for (const message of messages) {
@@ -106,22 +107,18 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
106
107
 
107
108
  const sendBatch = queue.sendBatch;
108
109
 
109
- if (sendBatch !== undefined) {
110
- yield* tryQueuePromise(definition.binding, "sendBatch", () =>
111
- sendBatch.call(queue, encodedMessages, options),
112
- );
113
-
114
- return;
110
+ if (sendBatch === undefined) {
111
+ return yield* new QueueOperationError({
112
+ binding: definition.binding,
113
+ operation: "sendBatch",
114
+ cause: new Error(`Queue binding "${definition.binding}" does not provide sendBatch()`),
115
+ message: `Cloudflare queue binding "${definition.binding}" does not provide sendBatch()`,
116
+ });
115
117
  }
116
118
 
117
- yield* tryQueuePromise(definition.binding, "sendBatch", async () => {
118
- for (const message of encodedMessages) {
119
- await queue.send(message.body, {
120
- contentType: message.contentType,
121
- delaySeconds: message.delaySeconds ?? options?.delaySeconds,
122
- });
123
- }
124
- });
119
+ yield* tryQueuePromise(definition.binding, "sendBatch", () =>
120
+ sendBatch.call(queue, encodedMessages, options),
121
+ );
125
122
  }),
126
123
  metrics: () => {
127
124
  const metrics = queue.metrics;
@@ -139,7 +136,7 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
139
136
 
140
137
  return tryQueuePromise(definition.binding, "metrics", () => metrics.call(queue));
141
138
  },
142
- unsafeRaw: Effect.succeed(queue as CloudflareQueue<EncodedBody>),
139
+ rawUnsafe: Effect.succeed(queue as CloudflareQueue<EncodedBody>),
143
140
  });
144
141
  };
145
142
 
@@ -44,7 +44,11 @@ export interface TagClass<
44
44
  Self,
45
45
  Id extends string,
46
46
  Message extends RpcDefinition.ServiceFreeSchema,
47
- > extends Context.ServiceClass<Self, Id, QueueBinding.QueueBindingClient<Message>> {
47
+ > extends Context.ServiceClass<
48
+ Self,
49
+ `effect-cf/Queue/${Id}`,
50
+ QueueBinding.QueueBindingClient<Message>
51
+ > {
48
52
  readonly id: Id;
49
53
  readonly message: Message;
50
54
  readonly make: <ROut, LayerError>(
@@ -71,7 +75,7 @@ export interface TagClass<
71
75
  QueueBinding.QueueOperationError,
72
76
  Self
73
77
  >;
74
- readonly unsafeRaw: () => Effect.Effect<globalThis.Queue<S.Codec.Encoded<Message>>, never, Self>;
78
+ readonly rawUnsafe: Effect.Effect<globalThis.Queue<S.Codec.Encoded<Message>>, never, Self>;
75
79
  }
76
80
 
77
81
  const makeDefinition = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
@@ -108,7 +112,9 @@ export const Tag =
108
112
  definition: { readonly message: Message },
109
113
  ) => {
110
114
  const queueDefinition = makeDefinition(id, definition);
111
- const tag = Context.Service<Self, QueueBinding.QueueBindingClient<Message>>()(id);
115
+ const tag = Context.Service<Self, QueueBinding.QueueBindingClient<Message>>()(
116
+ `effect-cf/Queue/${id}` as const,
117
+ );
112
118
 
113
119
  const layer = (binding: LayerOptions) =>
114
120
  QueueBinding.layer(tag, {
@@ -140,11 +146,7 @@ export const Tag =
140
146
  return yield* queue.metrics();
141
147
  });
142
148
 
143
- const unsafeRaw = Effect.fnUntraced(function* () {
144
- const queue = yield* tag;
145
-
146
- return yield* queue.unsafeRaw;
147
- });
149
+ const rawUnsafe = Effect.flatMap(tag, (queue) => queue.rawUnsafe);
148
150
 
149
151
  return Object.assign(tag, {
150
152
  id: queueDefinition.id,
@@ -154,7 +156,7 @@ export const Tag =
154
156
  send,
155
157
  sendBatch,
156
158
  metrics,
157
- unsafeRaw,
159
+ rawUnsafe,
158
160
  }) as TagClass<Self, Id, Message>;
159
161
  };
160
162
 
@@ -166,12 +168,11 @@ const wrapHandler = <ROut, const Self extends Definition.Any>(
166
168
  ): QueueEntrypoint.QueueHandler<ROut> => {
167
169
  const decodeBody = S.decodeUnknownEffect(definition.message);
168
170
 
169
- return (batch) =>
170
- Effect.gen(function* () {
171
- const decoded = yield* QueueEntrypoint.decodeBatch(batch.raw, decodeBody);
171
+ return Effect.fnUntraced(function* (batch: QueueEntrypoint.QueueBatch<unknown>) {
172
+ const decoded = yield* QueueEntrypoint.decodeBatch(batch.raw, decodeBody);
172
173
 
173
- yield* handler(decoded);
174
- });
174
+ yield* handler(decoded);
175
+ });
175
176
  };
176
177
 
177
178
  export const implement = <ROut, const Self extends Definition.Any>(
package/src/R2.ts CHANGED
@@ -16,16 +16,39 @@ import { Context, Data, Effect, Option, type Layer } from "effect";
16
16
 
17
17
  import * as Binding from "./Binding";
18
18
  import type { WorkerEnvironment } from "./Environment";
19
+ import * as ErrorMessage from "./internal/ErrorMessage";
19
20
 
20
21
  const expectedR2Bucket =
21
22
  "R2 bucket binding with head(), get(), put(), createMultipartUpload(), resumeMultipartUpload(), delete(), and list()";
22
23
 
24
+ /** R2 operation represented by {@link R2OperationError}. */
25
+ export type R2Operation =
26
+ | "head"
27
+ | "get"
28
+ | "put"
29
+ | "delete"
30
+ | "list"
31
+ | "createMultipartUpload"
32
+ | "resumeMultipartUpload"
33
+ | "uploadPart"
34
+ | "abortMultipartUpload"
35
+ | "completeMultipartUpload"
36
+ | "arrayBuffer"
37
+ | "bytes"
38
+ | "text"
39
+ | "json"
40
+ | "blob";
41
+
23
42
  /** Error raised when an R2 operation fails. */
24
43
  export class R2OperationError extends Data.TaggedError("R2OperationError")<{
25
44
  readonly binding: string;
26
- readonly operation: string;
45
+ readonly operation: R2Operation;
27
46
  readonly cause: unknown;
28
- }> {}
47
+ }> {
48
+ override get message(): string {
49
+ return `R2 ${this.operation} failed for binding "${this.binding}": ${ErrorMessage.causeMessage(this.cause)}`;
50
+ }
51
+ }
29
52
 
30
53
  /** Typed R2 bucket binding definition. */
31
54
  export interface R2Definition {
@@ -104,7 +127,7 @@ export interface R2Client {
104
127
  ) => Effect.Effect<R2MultipartUploadClient, R2OperationError>;
105
128
  readonly delete: (keys: string | ReadonlyArray<string>) => Effect.Effect<void, R2OperationError>;
106
129
  readonly list: (options?: R2ListOptions) => Effect.Effect<CloudflareR2Objects, R2OperationError>;
107
- readonly unsafeRaw: Effect.Effect<CloudflareR2Bucket>;
130
+ readonly rawUnsafe: Effect.Effect<CloudflareR2Bucket>;
108
131
  readonly definition: R2Definition;
109
132
  }
110
133
 
@@ -136,12 +159,12 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
136
159
  >;
137
160
  }
138
161
 
139
- const r2Error = (binding: string, operation: string, cause: unknown) =>
162
+ const r2Error = (binding: string, operation: R2Operation, cause: unknown) =>
140
163
  new R2OperationError({ binding, operation, cause });
141
164
 
142
165
  const tryR2Promise = <A>(
143
166
  binding: string,
144
- operation: string,
167
+ operation: R2Operation,
145
168
  evaluate: () => Promise<A>,
146
169
  ): Effect.Effect<A, R2OperationError> =>
147
170
  Effect.tryPromise({
@@ -151,7 +174,7 @@ const tryR2Promise = <A>(
151
174
 
152
175
  const tryR2Sync = <A>(
153
176
  binding: string,
154
- operation: string,
177
+ operation: R2Operation,
155
178
  evaluate: () => A,
156
179
  ): Effect.Effect<A, R2OperationError> =>
157
180
  Effect.try({
@@ -159,8 +182,9 @@ const tryR2Sync = <A>(
159
182
  catch: (cause) => r2Error(binding, operation, cause),
160
183
  });
161
184
 
162
- const maybe = <A>(value: A | null): Option.Option<A> =>
163
- value === null ? Option.none() : Option.some(value);
185
+ const spanOptions = (binding: string, operation: R2Operation) => ({
186
+ attributes: { binding, operation },
187
+ });
164
188
 
165
189
  const isR2ObjectBody = (
166
190
  value: CloudflareR2ObjectBody | CloudflareR2Object,
@@ -191,11 +215,22 @@ const wrapObjectBody = (binding: string, object: CloudflareR2ObjectBody): R2Obje
191
215
  get bodyUsed() {
192
216
  return object.bodyUsed;
193
217
  },
194
- arrayBuffer: tryR2Promise(binding, "arrayBuffer", () => object.arrayBuffer()),
195
- bytes: tryR2Promise(binding, "bytes", () => object.bytes()),
196
- text: tryR2Promise(binding, "text", () => object.text()),
197
- json: <T = unknown>() => tryR2Promise(binding, "json", () => object.json<T>()),
198
- blob: tryR2Promise(binding, "blob", () => object.blob()),
218
+ arrayBuffer: tryR2Promise(binding, "arrayBuffer", () => object.arrayBuffer()).pipe(
219
+ Effect.withSpan("R2.arrayBuffer", spanOptions(binding, "arrayBuffer")),
220
+ ),
221
+ bytes: tryR2Promise(binding, "bytes", () => object.bytes()).pipe(
222
+ Effect.withSpan("R2.bytes", spanOptions(binding, "bytes")),
223
+ ),
224
+ text: tryR2Promise(binding, "text", () => object.text()).pipe(
225
+ Effect.withSpan("R2.text", spanOptions(binding, "text")),
226
+ ),
227
+ json: <T = unknown>() =>
228
+ tryR2Promise(binding, "json", () => object.json<T>()).pipe(
229
+ Effect.withSpan("R2.json", spanOptions(binding, "json")),
230
+ ),
231
+ blob: tryR2Promise(binding, "blob", () => object.blob()).pipe(
232
+ Effect.withSpan("R2.blob", spanOptions(binding, "blob")),
233
+ ),
199
234
  });
200
235
 
201
236
  const wrapGetResult = (
@@ -234,29 +269,55 @@ export const makeClient = (
234
269
  raw: upload,
235
270
  key: upload.key,
236
271
  uploadId: upload.uploadId,
237
- uploadPart: (partNumber, value, options) =>
272
+ uploadPart: Effect.fn(
273
+ "R2.uploadPart",
274
+ spanOptions(definition.binding, "uploadPart"),
275
+ )((partNumber: number, value: R2UploadPartValue, options?: R2UploadPartOptions) =>
238
276
  tryR2Promise(definition.binding, "uploadPart", () =>
239
277
  upload.uploadPart(partNumber, value, options),
240
278
  ),
241
- abort: tryR2Promise(definition.binding, "abortMultipartUpload", () => upload.abort()),
242
- complete: (uploadedParts) =>
279
+ ),
280
+ abort: tryR2Promise(definition.binding, "abortMultipartUpload", () => upload.abort()).pipe(
281
+ Effect.withSpan(
282
+ "R2.abortMultipartUpload",
283
+ spanOptions(definition.binding, "abortMultipartUpload"),
284
+ ),
285
+ ),
286
+ complete: Effect.fn(
287
+ "R2.completeMultipartUpload",
288
+ spanOptions(definition.binding, "completeMultipartUpload"),
289
+ )((uploadedParts: ReadonlyArray<CloudflareR2UploadedPart>) =>
243
290
  tryR2Promise(definition.binding, "completeMultipartUpload", () =>
244
291
  upload.complete([...uploadedParts]),
245
292
  ),
293
+ ),
246
294
  });
247
295
 
248
296
  return (bucket) => {
249
- const get = ((key: string, options?: R2GetOptions) =>
297
+ const get = Effect.fn(
298
+ "R2.get",
299
+ spanOptions(definition.binding, "get"),
300
+ )((key: string, options?: R2GetOptions) =>
250
301
  tryR2Promise(definition.binding, "get", () => bucket.get(key, options)).pipe(
251
- Effect.map((object) => maybe(wrapGetResult(definition.binding, object))),
252
- )) as R2Client["get"];
302
+ Effect.map((object) => Option.fromNullOr(wrapGetResult(definition.binding, object))),
303
+ ),
304
+ ) as R2Client["get"];
253
305
 
254
306
  return {
255
307
  definition,
256
- head: (key) =>
257
- tryR2Promise(definition.binding, "head", () => bucket.head(key)).pipe(Effect.map(maybe)),
308
+ head: Effect.fn(
309
+ "R2.head",
310
+ spanOptions(definition.binding, "head"),
311
+ )((key: string) =>
312
+ tryR2Promise(definition.binding, "head", () => bucket.head(key)).pipe(
313
+ Effect.map(Option.fromNullOr),
314
+ ),
315
+ ),
258
316
  get,
259
- put: ((key: string, value: R2PutValue, options?: R2PutOptions) =>
317
+ put: Effect.fn(
318
+ "R2.put",
319
+ spanOptions(definition.binding, "put"),
320
+ )((key: string, value: R2PutValue, options?: R2PutOptions) =>
260
321
  tryR2Promise(definition.binding, "put", () => bucket.put(key, value, options)).pipe(
261
322
  Effect.map((object) => {
262
323
  if (object === null) {
@@ -269,22 +330,39 @@ export const makeClient = (
269
330
 
270
331
  return object;
271
332
  }),
272
- )) as R2Client["put"],
273
- createMultipartUpload: (key, options) =>
333
+ ),
334
+ ) as R2Client["put"],
335
+ createMultipartUpload: Effect.fn(
336
+ "R2.createMultipartUpload",
337
+ spanOptions(definition.binding, "createMultipartUpload"),
338
+ )((key: string, options?: R2MultipartOptions) =>
274
339
  tryR2Promise(definition.binding, "createMultipartUpload", () =>
275
340
  bucket.createMultipartUpload(key, options),
276
341
  ).pipe(Effect.map(wrapUpload)),
277
- resumeMultipartUpload: (key, uploadId) =>
342
+ ),
343
+ resumeMultipartUpload: Effect.fn(
344
+ "R2.resumeMultipartUpload",
345
+ spanOptions(definition.binding, "resumeMultipartUpload"),
346
+ )((key: string, uploadId: string) =>
278
347
  tryR2Sync(definition.binding, "resumeMultipartUpload", () =>
279
348
  wrapUpload(bucket.resumeMultipartUpload(key, uploadId)),
280
349
  ),
281
- delete: (keys) => {
350
+ ),
351
+ delete: Effect.fn(
352
+ "R2.delete",
353
+ spanOptions(definition.binding, "delete"),
354
+ )((keys: string | ReadonlyArray<string>) => {
282
355
  const nativeKeys = typeof keys === "string" ? keys : [...keys];
283
356
 
284
357
  return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
285
- },
286
- list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
287
- unsafeRaw: Effect.succeed(bucket),
358
+ }),
359
+ list: Effect.fn(
360
+ "R2.list",
361
+ spanOptions(definition.binding, "list"),
362
+ )((options?: R2ListOptions) =>
363
+ tryR2Promise(definition.binding, "list", () => bucket.list(options)),
364
+ ),
365
+ rawUnsafe: Effect.succeed(bucket),
288
366
  };
289
367
  };
290
368
  };
package/src/Rpc.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { RpcStub, RpcTarget } from "cloudflare:workers";
2
2
  import { Effect, type Scope } from "effect";
3
3
 
4
+ import { decodeWireError } from "./RpcDefinition";
5
+
4
6
  export { RpcStub, RpcTarget };
5
7
 
6
8
  export type Stubable = Rpc.Stubable;
@@ -113,7 +115,7 @@ export const resolve = <A>(value: A): Effect.Effect<Awaited<A>, unknown> =>
113
115
  isPromiseLike(value)
114
116
  ? Effect.tryPromise({
115
117
  try: () => value,
116
- catch: (cause) => cause,
118
+ catch: (cause) => decodeWireError(cause),
117
119
  })
118
120
  : Effect.sync(() => value as Awaited<A>);
119
121