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
package/src/Email.ts CHANGED
@@ -9,6 +9,7 @@ import { Context, Data, Effect, type Layer } from "effect";
9
9
 
10
10
  import * as Binding from "./Binding";
11
11
  import type { WorkerEnvironment } from "./Environment";
12
+ import * as ErrorMessage from "./internal/ErrorMessage";
12
13
 
13
14
  const expectedSendEmailBinding = "Send Email binding with send()";
14
15
  const textEncoder = new TextEncoder();
@@ -27,7 +28,14 @@ export const sendLimits = {
27
28
  maxAllowlistHeaders: 20,
28
29
  } as const;
29
30
 
30
- /** Error codes reported by Cloudflare Email Sending. */
31
+ /**
32
+ * Error codes reported by Cloudflare Email Sending.
33
+ *
34
+ * Surfaced on {@link EmailOperationError} `code` on a best-effort basis: the
35
+ * runtime attaches them as a non-standard `code` property on thrown errors,
36
+ * which may be missing depending on the runtime or dev-time proxying
37
+ * (miniflare remote bindings serialize errors and can strip it).
38
+ */
31
39
  export const emailErrorCodes = [
32
40
  "E_VALIDATION_ERROR",
33
41
  "E_FIELD_MISSING",
@@ -59,9 +67,21 @@ export class EmailOperationError extends Data.TaggedError("EmailOperationError")
59
67
  readonly binding: string;
60
68
  readonly operation: string;
61
69
  readonly cause: unknown;
62
- /** Cloudflare error code taken from the thrown error, when present. */
70
+ /**
71
+ * Cloudflare error code taken from the thrown error, when present.
72
+ *
73
+ * Best-effort: may be `undefined` depending on the runtime or dev-time
74
+ * proxying — miniflare remote bindings (`"remote": true`) serialize thrown
75
+ * errors and can strip the non-standard `code` property.
76
+ */
63
77
  readonly code?: EmailErrorCode | (string & {});
64
- }> {}
78
+ }> {
79
+ override get message(): string {
80
+ const code = this.code === undefined ? "" : ` (${this.code})`;
81
+
82
+ return `Email ${this.operation} failed for binding "${this.binding}"${code}: ${ErrorMessage.causeMessage(this.cause)}`;
83
+ }
84
+ }
65
85
 
66
86
  /** A single message field that violates a documented Email Sending limit. */
67
87
  export interface EmailViolation {
@@ -76,7 +96,11 @@ export class EmailValidationError extends Data.TaggedError("EmailValidationError
76
96
  readonly binding: string;
77
97
  readonly operation: string;
78
98
  readonly violations: ReadonlyArray<EmailViolation>;
79
- }> {}
99
+ }> {
100
+ override get message(): string {
101
+ return `Email ${this.operation} for binding "${this.binding}" failed validation: ${ErrorMessage.violationsMessage(this.violations)}`;
102
+ }
103
+ }
80
104
 
81
105
  /** Typed Cloudflare Send Email binding definition. */
82
106
  export interface EmailDefinition {
@@ -108,7 +132,7 @@ export interface EmailClient {
108
132
  (message: EmailMessage): Effect.Effect<EmailSendResult, EmailSendError>;
109
133
  (builder: EmailMessageBuilder): Effect.Effect<EmailSendResult, EmailSendError>;
110
134
  };
111
- readonly unsafeRaw: Effect.Effect<EmailBinding>;
135
+ readonly rawUnsafe: Effect.Effect<EmailBinding>;
112
136
  readonly definition: EmailDefinition;
113
137
  }
114
138
 
@@ -141,6 +165,12 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
141
165
  >;
142
166
  }
143
167
 
168
+ /**
169
+ * Best-effort extraction of the Cloudflare error `code` from a thrown error.
170
+ * Returns `undefined` when the property is absent — e.g. when a dev-time
171
+ * proxy such as a miniflare remote binding (`"remote": true`) serializes the
172
+ * error and strips non-standard properties.
173
+ */
144
174
  const emailErrorCode = (cause: unknown): EmailOperationError["code"] => {
145
175
  if (typeof cause !== "object" || cause === null) {
146
176
  return undefined;
@@ -439,21 +469,18 @@ export const makeClient =
439
469
  (email: EmailBinding): EmailClient => {
440
470
  const runtime = email as EmailRuntimeBinding;
441
471
  const validate = options?.validate ?? true;
442
- const send = ((message: EmailSendInput) => {
443
- const dispatch = () =>
444
- tryEmailPromise(definition.binding, "send", () => runtime.send(message));
445
-
446
- if (!validate || !isEmailMessageBuilder(message)) {
447
- return dispatch();
472
+ const send = Effect.fn("Email.send")(function* (message: EmailSendInput) {
473
+ if (validate && isEmailMessageBuilder(message)) {
474
+ yield* validateSendInput(definition.binding, "send", message);
448
475
  }
449
476
 
450
- return validateSendInput(definition.binding, "send", message).pipe(Effect.flatMap(dispatch));
477
+ return yield* tryEmailPromise(definition.binding, "send", () => runtime.send(message));
451
478
  }) as EmailClient["send"];
452
479
 
453
480
  return {
454
481
  definition,
455
482
  send,
456
- unsafeRaw: Effect.succeed(email),
483
+ rawUnsafe: Effect.succeed(email),
457
484
  };
458
485
  };
459
486
 
@@ -1,4 +1,4 @@
1
- import { Config, ConfigProvider, Context, Effect } from "effect";
1
+ import { Config, ConfigProvider, Context, Effect, type Layer } from "effect";
2
2
 
3
3
  /** Cloudflare worker environment object (`env`). */
4
4
  export type WorkerEnv = Cloudflare.Env;
@@ -72,8 +72,25 @@ export namespace WorkerConfig {
72
72
  /** Read a scalar Cloudflare var or secret as a boolean. */
73
73
  export const boolean = <const Name extends Key>(name: Name) => Config.boolean(name);
74
74
 
75
- /** Build a `ConfigProvider` from a Cloudflare worker `env` object. */
76
- export const providerFromEnv = (env: WorkerEnv) => ConfigProvider.fromUnknown(env);
75
+ /** Options for building a `ConfigProvider` from a Cloudflare worker `env` object. */
76
+ export interface ProviderOptions {
77
+ /**
78
+ * Keep empty-string env values as explicit `""` config values.
79
+ *
80
+ * By default (absent or `false`), empty strings are treated as missing
81
+ * config, matching `ConfigProvider.fromUnknown`.
82
+ */
83
+ readonly preserveEmptyStrings?: boolean;
84
+ }
85
+
86
+ /**
87
+ * Build a `ConfigProvider` from a Cloudflare worker `env` object.
88
+ *
89
+ * Empty-string values are treated as missing config by default; pass
90
+ * `{ preserveEmptyStrings: true }` to keep them as explicit values.
91
+ */
92
+ export const providerFromEnv = (env: WorkerEnv, options?: ProviderOptions) =>
93
+ ConfigProvider.fromUnknown(env, options);
77
94
 
78
95
  /**
79
96
  * Build a `ConfigProvider` from the current `WorkerEnvironment` with a custom
@@ -89,12 +106,15 @@ export namespace WorkerConfig {
89
106
  * Replace the active Effect `ConfigProvider` with one backed by the current
90
107
  * Cloudflare worker `env` object.
91
108
  */
92
- export const providerLayer = ConfigProvider.layer(provider);
109
+ export const providerLayer: Layer.Layer<never, never, WorkerEnvironment> =
110
+ ConfigProvider.layer(provider);
93
111
 
94
112
  /** Alias for `providerLayer` for concise use in worker layers. */
95
- export const layer = providerLayer;
113
+ export const layer: Layer.Layer<never, never, WorkerEnvironment> = providerLayer;
96
114
 
97
115
  /** Build a `ConfigProvider` layer with a custom `env` conversion function. */
98
- export const layerWith = (makeProvider: (env: WorkerEnv) => ConfigProvider.ConfigProvider) =>
116
+ export const layerWith = (
117
+ makeProvider: (env: WorkerEnv) => ConfigProvider.ConfigProvider,
118
+ ): Layer.Layer<never, never, WorkerEnvironment> =>
99
119
  ConfigProvider.layer(providerWith(makeProvider));
100
120
  }
package/src/Hyperdrive.ts CHANGED
@@ -13,7 +13,7 @@ export interface HyperdriveDefinition {
13
13
 
14
14
  export interface HyperdriveClient {
15
15
  readonly connectionString: string;
16
- readonly unsafeRaw: Effect.Effect<Hyperdrive>;
16
+ readonly rawUnsafe: Effect.Effect<Hyperdrive>;
17
17
  readonly definition: HyperdriveDefinition;
18
18
  }
19
19
 
@@ -60,7 +60,7 @@ export const makeClient =
60
60
  (hyperdrive: Hyperdrive): HyperdriveClient => ({
61
61
  definition,
62
62
  connectionString: hyperdrive.connectionString,
63
- unsafeRaw: Effect.succeed(hyperdrive),
63
+ rawUnsafe: Effect.succeed(hyperdrive),
64
64
  });
65
65
 
66
66
  export const layer = <Self>(
@@ -1,27 +1,39 @@
1
1
  import { PgClient } from "@effect/sql-pg";
2
2
  import { Effect, Layer, Redacted } from "effect";
3
+ import type { SqlClient, SqlError } from "effect/unstable/sql";
3
4
 
5
+ import type * as Binding from "./Binding";
6
+ import type { WorkerEnvironment } from "./Environment";
4
7
  import type * as Hyperdrive from "./Hyperdrive";
5
8
 
9
+ /** Hyperdrive owns database pooling, so app-side pool and connection options are not exposed. */
6
10
  export type PgLayerOptions = Omit<
7
- PgClient.PgClientConfig & { readonly acquireForStream?: boolean },
11
+ Parameters<typeof PgClient.makeClient>[0],
8
12
  "url" | "host" | "port" | "path" | "ssl" | "database" | "username" | "password" | "stream"
9
13
  >;
10
14
 
15
+ /**
16
+ * Provides `PgClient` and `SqlClient` backed by a Hyperdrive binding.
17
+ *
18
+ * Note: since Effect `4.0.0-beta.99`, `PgClient.makeClient` connects eagerly,
19
+ * so the TCP connection to Hyperdrive is opened when this layer is built.
20
+ * In per-request Workers runtimes, build the layer within the request
21
+ * lifecycle rather than sharing it across requests.
22
+ */
11
23
  export const layer = <Self, Id extends string>(
12
24
  tag: Hyperdrive.TagClass<Self, Id>,
13
25
  binding: Hyperdrive.LayerOptions,
14
26
  options?: PgLayerOptions,
15
- ) =>
16
- Layer.unwrap(
17
- Effect.gen(function* () {
18
- const hyperdrive = yield* tag;
19
-
20
- return PgClient.layerFrom(
21
- PgClient.makeClient({
22
- ...options,
23
- url: Redacted.make(hyperdrive.connectionString),
24
- }),
25
- );
26
- }),
27
+ ): Layer.Layer<
28
+ PgClient.PgClient | SqlClient.SqlClient,
29
+ Binding.BindingNotFoundError | Binding.BindingValidationError | SqlError.SqlError,
30
+ WorkerEnvironment
31
+ > =>
32
+ PgClient.layerFrom(
33
+ Effect.flatMap(tag, (hyperdrive) =>
34
+ PgClient.makeClient({
35
+ ...options,
36
+ url: Redacted.make(hyperdrive.connectionString),
37
+ }),
38
+ ),
27
39
  ).pipe(Layer.provide(tag.layer(binding)));
package/src/Images.ts CHANGED
@@ -16,12 +16,16 @@ import type {
16
16
  ImageUploadOptions as CloudflareImageUploadOptions,
17
17
  Response as CloudflareResponse,
18
18
  } from "@cloudflare/workers-types";
19
- import { Context, Data, Effect, Option, type Layer } from "effect";
19
+ import { Context, Data, Effect, Function, Option, type Layer } from "effect";
20
20
 
21
21
  import * as Binding from "./Binding";
22
22
  import type { WorkerEnvironment } from "./Environment";
23
+ import * as ErrorMessage from "./internal/ErrorMessage";
24
+
25
+ const TypeId = "~effect-cf/Images/Steps" as const;
26
+
27
+ export type TypeId = typeof TypeId;
23
28
 
24
- const TypeId = "effect-cf/Images/Steps" as const;
25
29
  const expectedImagesBinding = "Images binding with info() and input()";
26
30
 
27
31
  /** Error raised when a Cloudflare Images operation fails. */
@@ -29,7 +33,11 @@ export class ImagesOperationError extends Data.TaggedError("ImagesOperationError
29
33
  readonly binding: string;
30
34
  readonly operation: string;
31
35
  readonly cause: unknown;
32
- }> {}
36
+ }> {
37
+ override get message(): string {
38
+ return `Images ${this.operation} failed for binding "${this.binding}": ${ErrorMessage.causeMessage(this.cause)}`;
39
+ }
40
+ }
33
41
 
34
42
  /** Typed Cloudflare Images binding definition. */
35
43
  export interface ImagesDefinition {
@@ -64,6 +72,8 @@ export type Step = Data.TaggedEnum<{
64
72
  readonly Draw: DrawStepOptions;
65
73
  }>;
66
74
 
75
+ export const Step = Data.taggedEnum<Step>();
76
+
67
77
  export interface Steps {
68
78
  readonly [TypeId]: typeof TypeId;
69
79
  readonly steps: ReadonlyArray<Step>;
@@ -101,7 +111,7 @@ export interface HostedImagesClient {
101
111
  options?: ImageUploadOptions,
102
112
  ) => Effect.Effect<ImageMetadata, ImagesOperationError>;
103
113
  readonly list: (options?: ImageListOptions) => Effect.Effect<ImageList, ImagesOperationError>;
104
- readonly unsafeRaw: Effect.Effect<CloudflareHostedImagesBinding>;
114
+ readonly rawUnsafe: Effect.Effect<CloudflareHostedImagesBinding>;
105
115
  }
106
116
 
107
117
  export interface ImagesRuntimeBinding {
@@ -130,7 +140,7 @@ export interface ImagesClient {
130
140
  options: ProcessOptions,
131
141
  ) => Effect.Effect<ImagesTransformationResultClient, ImagesOperationError>;
132
142
  readonly hosted: Option.Option<HostedImagesClient>;
133
- readonly unsafeRaw: Effect.Effect<ImagesRuntimeBinding>;
143
+ readonly rawUnsafe: Effect.Effect<ImagesRuntimeBinding>;
134
144
  readonly definition: ImagesDefinition;
135
145
  }
136
146
 
@@ -170,47 +180,23 @@ const makeSteps = (steps: ReadonlyArray<Step>): Steps => ({
170
180
  /** Empty Images transformation pipeline. */
171
181
  export const empty: Steps = makeSteps([]);
172
182
 
173
- export function transform(transform: ImageTransform): (steps: Steps) => Steps;
174
- export function transform(steps: Steps, transform: ImageTransform): Steps;
175
- export function transform(
176
- stepsOrTransform: Steps | ImageTransform,
177
- transformValue?: ImageTransform,
178
- ): Steps | ((steps: Steps) => Steps) {
179
- if (transformValue === undefined) {
180
- return (steps) => transform(steps, stepsOrTransform as ImageTransform);
181
- }
182
-
183
- const steps = stepsOrTransform as Steps;
184
-
185
- return makeSteps([
186
- ...steps.steps,
187
- {
188
- _tag: "Transform",
189
- transform: transformValue,
190
- },
191
- ]);
192
- }
193
-
194
- export function draw(draw: DrawStepOptions): (steps: Steps) => Steps;
195
- export function draw(steps: Steps, draw: DrawStepOptions): Steps;
196
- export function draw(
197
- stepsOrDraw: Steps | DrawStepOptions,
198
- drawValue?: DrawStepOptions,
199
- ): Steps | ((steps: Steps) => Steps) {
200
- if (drawValue === undefined) {
201
- return (steps) => draw(steps, stepsOrDraw as DrawStepOptions);
202
- }
203
-
204
- const steps = stepsOrDraw as Steps;
205
-
206
- return makeSteps([
207
- ...steps.steps,
208
- {
209
- _tag: "Draw",
210
- ...drawValue,
211
- },
212
- ]);
213
- }
183
+ export const transform: {
184
+ (transform: ImageTransform): (steps: Steps) => Steps;
185
+ (steps: Steps, transform: ImageTransform): Steps;
186
+ } = Function.dual(
187
+ 2,
188
+ (steps: Steps, transformValue: ImageTransform): Steps =>
189
+ makeSteps([...steps.steps, Step.Transform({ transform: transformValue })]),
190
+ );
191
+
192
+ export const draw: {
193
+ (draw: DrawStepOptions): (steps: Steps) => Steps;
194
+ (steps: Steps, draw: DrawStepOptions): Steps;
195
+ } = Function.dual(
196
+ 2,
197
+ (steps: Steps, drawValue: DrawStepOptions): Steps =>
198
+ makeSteps([...steps.steps, Step.Draw(drawValue)]),
199
+ );
214
200
 
215
201
  const imagesError = (binding: string, operation: string, cause: unknown) =>
216
202
  new ImagesOperationError({ binding, operation, cause });
@@ -266,10 +252,20 @@ const wrapResult = (
266
252
 
267
253
  const wrapHandle = (binding: string, handle: CloudflareImageHandle): ImageHandleClient => ({
268
254
  raw: handle,
269
- details: tryImagesPromise(binding, "details", () => handle.details()).pipe(Effect.map(maybe)),
270
- bytes: tryImagesPromise(binding, "bytes", () => handle.bytes()).pipe(Effect.map(maybe)),
271
- update: (options) => tryImagesPromise(binding, "update", () => handle.update(options)),
272
- delete: tryImagesPromise(binding, "delete", () => handle.delete()),
255
+ details: tryImagesPromise(binding, "details", () => handle.details()).pipe(
256
+ Effect.map(maybe),
257
+ Effect.withSpan("Images.details"),
258
+ ),
259
+ bytes: tryImagesPromise(binding, "bytes", () => handle.bytes()).pipe(
260
+ Effect.map(maybe),
261
+ Effect.withSpan("Images.bytes"),
262
+ ),
263
+ update: Effect.fn("Images.update")((options: ImageUpdateOptions) =>
264
+ tryImagesPromise(binding, "update", () => handle.update(options)),
265
+ ),
266
+ delete: tryImagesPromise(binding, "delete", () => handle.delete()).pipe(
267
+ Effect.withSpan("Images.delete"),
268
+ ),
273
269
  });
274
270
 
275
271
  const wrapHosted = (
@@ -277,10 +273,13 @@ const wrapHosted = (
277
273
  hosted: CloudflareHostedImagesBinding,
278
274
  ): HostedImagesClient => ({
279
275
  image: (imageId) => wrapHandle(binding, hosted.image(imageId)),
280
- upload: (image, options) =>
276
+ upload: Effect.fn("Images.upload")((image: ImageUploadValue, options?: ImageUploadOptions) =>
281
277
  tryImagesPromise(binding, "upload", () => hosted.upload(image, options)),
282
- list: (options) => tryImagesPromise(binding, "list", () => hosted.list(options)),
283
- unsafeRaw: Effect.succeed(hosted),
278
+ ),
279
+ list: Effect.fn("Images.list")((options?: ImageListOptions) =>
280
+ tryImagesPromise(binding, "list", () => hosted.list(options)),
281
+ ),
282
+ rawUnsafe: Effect.succeed(hosted),
284
283
  });
285
284
 
286
285
  export const makeClient =
@@ -289,44 +288,40 @@ export const makeClient =
289
288
  const input = (image: ImageInputValue, options?: ImageInputOptions) =>
290
289
  tryImagesSync(definition.binding, "input", () => images.input(image, options));
291
290
 
292
- const process = (steps: Steps, options: ProcessOptions) =>
293
- Effect.gen(function* () {
294
- let transformer = yield* input(options.stream, options.inputOptions);
295
-
296
- for (const step of steps.steps) {
297
- switch (step._tag) {
298
- case "Draw": {
299
- transformer = yield* tryImagesSync(definition.binding, "draw", () =>
300
- transformer.draw(step.image, step.options),
301
- );
302
- break;
303
- }
304
- case "Transform": {
305
- transformer = yield* tryImagesSync(definition.binding, "transform", () =>
306
- transformer.transform(step.transform),
307
- );
308
- break;
309
- }
310
- }
311
- }
312
-
313
- const result = yield* tryImagesPromise(definition.binding, "output", () =>
314
- transformer.output(options.outputOptions),
315
- );
316
-
317
- return wrapResult(definition.binding, result);
318
- });
291
+ const process = Effect.fn("Images.process")(function* (steps: Steps, options: ProcessOptions) {
292
+ let transformer = yield* input(options.stream, options.inputOptions);
293
+
294
+ for (const step of steps.steps) {
295
+ transformer = yield* Step.$match(step, {
296
+ Draw: (drawStep) =>
297
+ tryImagesSync(definition.binding, "draw", () =>
298
+ transformer.draw(drawStep.image, drawStep.options),
299
+ ),
300
+ Transform: (transformStep) =>
301
+ tryImagesSync(definition.binding, "transform", () =>
302
+ transformer.transform(transformStep.transform),
303
+ ),
304
+ });
305
+ }
306
+
307
+ const result = yield* tryImagesPromise(definition.binding, "output", () =>
308
+ transformer.output(options.outputOptions),
309
+ );
310
+
311
+ return wrapResult(definition.binding, result);
312
+ });
319
313
 
320
314
  return {
321
315
  definition,
322
- info: (image, options) =>
316
+ info: Effect.fn("Images.info")((image: ImageInputValue, options?: ImageInputOptions) =>
323
317
  tryImagesPromise(definition.binding, "info", () => images.info(image, options)),
318
+ ),
324
319
  input,
325
320
  process,
326
321
  hosted: isHostedImagesBinding(images.hosted)
327
322
  ? Option.some(wrapHosted(definition.binding, images.hosted))
328
323
  : Option.none(),
329
- unsafeRaw: Effect.succeed(images),
324
+ rawUnsafe: Effect.succeed(images),
330
325
  };
331
326
  };
332
327
 
package/src/Kv.ts CHANGED
@@ -6,16 +6,24 @@ import { Context, Data, Effect, Option, Schema as S, type Layer } from "effect";
6
6
 
7
7
  import * as Binding from "./Binding";
8
8
  import type { WorkerEnvironment } from "./Environment";
9
+ import * as ErrorMessage from "./internal/ErrorMessage";
9
10
 
10
11
  const expectedKvNamespace =
11
12
  "KV namespace binding with get(), put(), delete(), getWithMetadata(), and list()";
12
13
 
14
+ /** KV operation represented by {@link KvOperationError}. */
15
+ export type KvOperation = "put" | "get" | "getWithMetadata" | "list" | "delete";
16
+
13
17
  /** Error raised when a KV operation fails. */
14
18
  export class KvOperationError extends Data.TaggedError("KvOperationError")<{
15
19
  readonly binding: string;
16
- readonly operation: string;
20
+ readonly operation: KvOperation;
17
21
  readonly cause: unknown;
18
- }> {}
22
+ }> {
23
+ override get message(): string {
24
+ return `Kv ${this.operation} failed for binding "${this.binding}": ${ErrorMessage.causeMessage(this.cause)}`;
25
+ }
26
+ }
19
27
 
20
28
  /** `KVNamespace.put` options. */
21
29
  export type KvPutOptions = CloudflareKVNamespacePutOptions;
@@ -95,8 +103,8 @@ export interface KvClient<Key, Value, EncodedValue> {
95
103
  readonly list: <Metadata = unknown>(
96
104
  options?: KvListOptions<Metadata>,
97
105
  ) => Effect.Effect<KvListResult<Key, Metadata>, KvOperationError | S.SchemaError>;
98
- readonly remove: (key: Key) => Effect.Effect<void, KvOperationError | S.SchemaError>;
99
- readonly unsafeRaw: Effect.Effect<KVNamespace>;
106
+ readonly delete: (key: Key) => Effect.Effect<void, KvOperationError | S.SchemaError>;
107
+ readonly rawUnsafe: Effect.Effect<KVNamespace>;
100
108
  readonly definition: KvDefinition<Key, Value, EncodedValue>;
101
109
  }
102
110
 
@@ -123,18 +131,12 @@ export interface TagClass<
123
131
  >;
124
132
  }
125
133
 
126
- const maybeString = (value: string | null | undefined): Option.Option<string> =>
127
- value === null || value === undefined ? Option.none() : Option.some(value);
128
-
129
- const maybeNumber = (value: number | undefined): Option.Option<number> =>
130
- value === undefined ? Option.none() : Option.some(value);
131
-
132
- const kvError = (binding: string, operation: string, cause: unknown) =>
134
+ const kvError = (binding: string, operation: KvOperation, cause: unknown) =>
133
135
  new KvOperationError({ binding, operation, cause });
134
136
 
135
137
  const tryKvPromise = <A>(
136
138
  binding: string,
137
- operation: string,
139
+ operation: KvOperation,
138
140
  evaluate: () => Promise<A>,
139
141
  ): Effect.Effect<A, KvOperationError> =>
140
142
  Effect.tryPromise({
@@ -163,12 +165,15 @@ export const makeClient = <Key, Value, EncodedValue>(
163
165
  ): ((kv: KVNamespace) => KvClient<Key, Value, EncodedValue>) => {
164
166
  const encodeKey = S.encodeEffect(definition.key);
165
167
  const decodeKey = S.decodeUnknownEffect(definition.key);
166
- const encodeValue = S.encodeEffect(S.fromJsonString(S.toCodecJson(definition.value)));
167
- const decodeValue = S.decodeUnknownEffect(S.fromJsonString(S.toCodecJson(definition.value)));
168
+ const jsonValue = S.fromJsonString(S.toCodecJson(definition.value));
169
+ const encodeValue = S.encodeEffect(jsonValue);
170
+ const decodeValue = S.decodeUnknownEffect(jsonValue);
168
171
 
169
172
  return (kv) => ({
170
173
  definition,
171
- put: Effect.fnUntraced(function* (key: Key, value: Value, options?: KvPutOptions) {
174
+ put: Effect.fn("Kv.put", {
175
+ attributes: { binding: definition.binding, operation: "put" },
176
+ })(function* (key: Key, value: Value, options?: KvPutOptions) {
172
177
  const keyEncoded = yield* encodeKey(key);
173
178
  const valueEncoded = yield* encodeValue(value);
174
179
 
@@ -176,7 +181,9 @@ export const makeClient = <Key, Value, EncodedValue>(
176
181
  kv.put(keyEncoded, valueEncoded, options),
177
182
  );
178
183
  }),
179
- get: Effect.fnUntraced(function* (key: Key) {
184
+ get: Effect.fn("Kv.get", {
185
+ attributes: { binding: definition.binding, operation: "get" },
186
+ })(function* (key: Key) {
180
187
  const keyEncoded = yield* encodeKey(key);
181
188
  const valueEncoded = yield* tryKvPromise(definition.binding, "get", () => kv.get(keyEncoded));
182
189
 
@@ -186,10 +193,9 @@ export const makeClient = <Key, Value, EncodedValue>(
186
193
 
187
194
  return yield* decodeValue(valueEncoded).pipe(Effect.map(Option.some));
188
195
  }),
189
- getWithMetadata: Effect.fnUntraced(function* <Metadata>(
190
- key: Key,
191
- metadataSchema: S.Codec<Metadata, unknown>,
192
- ) {
196
+ getWithMetadata: Effect.fn("Kv.getWithMetadata", {
197
+ attributes: { binding: definition.binding, operation: "getWithMetadata" },
198
+ })(function* <Metadata>(key: Key, metadataSchema: S.Codec<Metadata, unknown>) {
193
199
  const keyEncoded = yield* encodeKey(key);
194
200
  const result = yield* tryKvPromise(definition.binding, "getWithMetadata", () =>
195
201
  kv.getWithMetadata<Metadata>(keyEncoded),
@@ -208,10 +214,12 @@ export const makeClient = <Key, Value, EncodedValue>(
208
214
  return Option.some({
209
215
  value,
210
216
  metadata,
211
- cacheStatus: maybeString(result.cacheStatus),
217
+ cacheStatus: Option.fromNullishOr(result.cacheStatus),
212
218
  });
213
219
  }),
214
- list: Effect.fnUntraced(function* <Metadata = unknown>(options?: KvListOptions<Metadata>) {
220
+ list: Effect.fn("Kv.list", {
221
+ attributes: { binding: definition.binding, operation: "list" },
222
+ })(function* <Metadata = unknown>(options?: KvListOptions<Metadata>) {
215
223
  const { metadataSchema, ...kvOptions } = options ?? {};
216
224
  const result = yield* tryKvPromise(definition.binding, "list", () =>
217
225
  kv.list<Metadata>(kvOptions),
@@ -229,7 +237,7 @@ export const makeClient = <Key, Value, EncodedValue>(
229
237
 
230
238
  keys.push({
231
239
  name: decodedName,
232
- expiration: maybeNumber(key.expiration),
240
+ expiration: Option.fromUndefinedOr(key.expiration),
233
241
  metadata: decodedMetadata,
234
242
  });
235
243
  }
@@ -237,16 +245,18 @@ export const makeClient = <Key, Value, EncodedValue>(
237
245
  return {
238
246
  keys,
239
247
  listComplete: result.list_complete,
240
- cursor: maybeString("cursor" in result ? result.cursor : undefined),
241
- cacheStatus: maybeString(result.cacheStatus),
248
+ cursor: Option.fromNullishOr("cursor" in result ? result.cursor : undefined),
249
+ cacheStatus: Option.fromNullishOr(result.cacheStatus),
242
250
  };
243
251
  }),
244
- remove: Effect.fnUntraced(function* (key: Key) {
252
+ delete: Effect.fn("Kv.delete", {
253
+ attributes: { binding: definition.binding, operation: "delete" },
254
+ })(function* (key: Key) {
245
255
  const keyEncoded = yield* encodeKey(key);
246
256
 
247
257
  yield* tryKvPromise(definition.binding, "delete", () => kv.delete(keyEncoded));
248
258
  }),
249
- unsafeRaw: Effect.succeed(kv),
259
+ rawUnsafe: Effect.succeed(kv),
250
260
  });
251
261
  };
252
262
 
package/src/Queue.ts CHANGED
@@ -3,6 +3,7 @@ import { Data, Effect, type Scope } from "effect";
3
3
  import type { ExecutionContext, WorkerContext } from "./Worker";
4
4
  import type { WorkerEnvironment } from "./Environment";
5
5
  import * as QueueDefinition from "./QueueDefinition";
6
+ import * as ErrorMessage from "./internal/ErrorMessage";
6
7
 
7
8
  export interface QueueMessage<Body> {
8
9
  readonly raw: globalThis.Message<unknown>;
@@ -28,7 +29,11 @@ export class QueueMessageDecodeError extends Data.TaggedError("QueueMessageDecod
28
29
  readonly messageId: string;
29
30
  readonly index: number;
30
31
  readonly cause: unknown;
31
- }> {}
32
+ }> {
33
+ override get message(): string {
34
+ return `Queue "${this.queue}" failed to decode message "${this.messageId}" at index ${this.index}: ${ErrorMessage.causeMessage(this.cause)}`;
35
+ }
36
+ }
32
37
 
33
38
  type RuntimeContext<ROut> = ExecutionContext | WorkerContext | WorkerEnvironment | ROut;
34
39