effect-cf 0.20.0 → 0.22.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 +56 -0
  2. package/README.md +2 -2
  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 +708 -549
  7. package/dist/index.mjs +946 -589
  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 +87 -55
  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 +6 -9
  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 +205 -198
  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 +14 -2
  52. package/src/Entry.ts +0 -259
  53. package/src/internal/Clock.ts +0 -37
@@ -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
 
@@ -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