effect-cf 0.9.0 → 0.9.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.9.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#30](https://github.com/danieljvdm/effect-cf/pull/30) [`29c3fd4`](https://github.com/danieljvdm/effect-cf/commit/29c3fd491bad0c8d6994e9a666f6e501c1a337a2) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Accept local queue producer bindings that only expose `send`, including Wrangler local dev bindings. Binding validation errors now include the binding name, expected shape, and actual resource shape in pretty output across Cloudflare bindings.
8
+
9
+ ## 0.9.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#27](https://github.com/danieljvdm/effect-cf/pull/27) [`ac2fb0f`](https://github.com/danieljvdm/effect-cf/commit/ac2fb0f9b557c1f73d779ed024c03245c1850b02) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Wrap R2 object body reader methods in Effect so `json`, `text`, `bytes`, `arrayBuffer`, and `blob` report read failures as `R2OperationError`.
14
+
3
15
  ## 0.9.0
4
16
 
5
17
  ### Minor Changes
@@ -33,21 +33,90 @@ const TypeId = "effect-cf/Binding";
33
33
  var BindingNotFoundError = class extends Data.TaggedError("BindingNotFoundError") {};
34
34
  /** Error raised when a binding exists but does not match the expected shape. */
35
35
  var BindingValidationError = class extends Data.TaggedError("BindingValidationError") {};
36
+ const defaultExpected = "Cloudflare binding resource";
36
37
  const isPropertyTarget = (value) => (typeof value === "object" || typeof value === "function") && value !== null;
37
- const getBinding = (env, binding, isResource) => Effect.gen(function* () {
38
- if (!isPropertyTarget(env)) return yield* Effect.fail(new BindingValidationError({ binding }));
38
+ const getObjectName = (value) => {
39
+ const tag = (() => {
40
+ try {
41
+ return Object.prototype.toString.call(value).slice(8, -1);
42
+ } catch {
43
+ return typeof value;
44
+ }
45
+ })();
46
+ const constructorName = (() => {
47
+ try {
48
+ return "constructor" in value && typeof value.constructor === "function" && typeof value.constructor.name === "string" ? value.constructor.name : void 0;
49
+ } catch {
50
+ return;
51
+ }
52
+ })();
53
+ if (tag !== "Object") return tag;
54
+ if (constructorName !== void 0 && constructorName !== "" && constructorName !== "Object") return constructorName;
55
+ return tag;
56
+ };
57
+ const propertyNames = (value) => {
58
+ const names = /* @__PURE__ */ new Set();
59
+ for (const target of [value, Object.getPrototypeOf(value)]) {
60
+ if (target === null || target === Object.prototype || target === Function.prototype) continue;
61
+ try {
62
+ for (const name of Object.getOwnPropertyNames(target)) names.add(name);
63
+ } catch {
64
+ continue;
65
+ }
66
+ }
67
+ return [...names].filter((name) => name !== "constructor").sort();
68
+ };
69
+ const isMethod = (value, name) => {
70
+ try {
71
+ return typeof Reflect.get(value, name) === "function";
72
+ } catch {
73
+ return false;
74
+ }
75
+ };
76
+ const describeActual = (value) => {
77
+ if (value === null) return "null";
78
+ if (typeof value !== "object" && typeof value !== "function") return typeof value;
79
+ const names = propertyNames(value);
80
+ const methods = names.filter((name) => isMethod(value, name));
81
+ const properties = names.filter((name) => !methods.includes(name));
82
+ const details = [methods.length > 0 ? `methods ${methods.join(", ")}` : void 0, properties.length > 0 ? `properties ${properties.join(", ")}` : void 0].filter((detail) => detail !== void 0);
83
+ if (details.length === 0) return getObjectName(value);
84
+ return `${getObjectName(value)} with ${details.join("; ")}`;
85
+ };
86
+ const getBinding = (env, binding, isResource, options) => Effect.gen(function* () {
87
+ if (!isPropertyTarget(env)) {
88
+ const actual = describeActual(env);
89
+ return yield* Effect.fail(new BindingValidationError({
90
+ binding,
91
+ expected: "WorkerEnvironment object",
92
+ actual,
93
+ message: `Cloudflare binding "${binding}" failed validation. Expected WorkerEnvironment object; got ${actual}`
94
+ }));
95
+ }
39
96
  const resource = Reflect.get(env, binding);
40
- if (resource === void 0) return yield* Effect.fail(new BindingNotFoundError({ binding }));
41
- if (!isResource(resource)) return yield* Effect.fail(new BindingValidationError({ binding }));
97
+ if (resource === void 0) return yield* Effect.fail(new BindingNotFoundError({
98
+ binding,
99
+ message: `Cloudflare binding "${binding}" was not found in WorkerEnvironment`
100
+ }));
101
+ if (!isResource(resource)) {
102
+ const expected = options?.expected ?? defaultExpected;
103
+ const actual = describeActual(resource);
104
+ return yield* Effect.fail(new BindingValidationError({
105
+ binding,
106
+ expected,
107
+ actual,
108
+ message: `Cloudflare binding "${binding}" failed validation. Expected ${expected}; got ${actual}`
109
+ }));
110
+ }
42
111
  return resource;
43
112
  });
44
- const layer$1 = (tag, binding, isResource, wrap) => Layer.effect(tag, Effect.gen(function* () {
45
- const resource = yield* getBinding(yield* WorkerEnvironment, binding, isResource);
113
+ const layer$1 = (tag, binding, isResource, wrap, options) => Layer.effect(tag, Effect.gen(function* () {
114
+ const resource = yield* getBinding(yield* WorkerEnvironment, binding, isResource, options);
46
115
  return wrap === void 0 ? resource : wrap(resource);
47
116
  }));
48
- const Service = () => (id, binding, isResource, wrap) => {
117
+ const Service = () => (id, binding, isResource, wrap, options) => {
49
118
  const tag = Context.Service()(id);
50
- const serviceLayer = layer$1(tag, binding, isResource, wrap);
119
+ const serviceLayer = layer$1(tag, binding, isResource, wrap, options);
51
120
  return Object.assign(tag, {
52
121
  [TypeId]: TypeId,
53
122
  binding,
@@ -63,6 +132,7 @@ var Hyperdrive_exports = /* @__PURE__ */ __exportAll({
63
132
  make: () => make,
64
133
  makeClient: () => makeClient
65
134
  });
135
+ const expectedHyperdrive = "Hyperdrive binding with connectionString";
66
136
  const isHyperdrive = (value) => {
67
137
  if (typeof value !== "object" || value === null) return false;
68
138
  return typeof value.connectionString === "string";
@@ -72,7 +142,7 @@ const makeClient = (definition) => (hyperdrive) => ({
72
142
  connectionString: hyperdrive.connectionString,
73
143
  unsafeRaw: Effect.succeed(hyperdrive)
74
144
  });
75
- const layer = (tag, definition) => layer$1(tag, definition.binding, isHyperdrive, makeClient(definition));
145
+ const layer = (tag, definition) => layer$1(tag, definition.binding, isHyperdrive, makeClient(definition), { expected: expectedHyperdrive });
76
146
  const make = (id) => Tag()(id);
77
147
  const Tag = () => (id) => {
78
148
  const tag = Context.Service()(id);
@@ -76,7 +76,7 @@ declare namespace WorkerConfig {
76
76
  const layerWith: (makeProvider: (env: WorkerEnv) => ConfigProvider.ConfigProvider) => _$effect_Layer0.Layer<never, never, WorkerEnvironment>;
77
77
  }
78
78
  declare namespace Binding_d_exports {
79
- export { BindingNotFoundError, BindingService, BindingValidationError, Service, TypeId, layer$1 as layer };
79
+ export { BindingNotFoundError, BindingService, BindingValidationError, Service, TypeId, ValidationOptions, layer$1 as layer };
80
80
  }
81
81
  /** Internal type id marker used by binding helper services. */
82
82
  declare const TypeId: "effect-cf/Binding";
@@ -86,6 +86,7 @@ declare const BindingNotFoundError_base: new <A extends Record<string, any> = {}
86
86
  /** Error raised when a configured binding does not exist on `env`. */
87
87
  declare class BindingNotFoundError extends BindingNotFoundError_base<{
88
88
  readonly binding: string;
89
+ readonly message: string;
89
90
  }> {}
90
91
  declare const BindingValidationError_base: new <A extends Record<string, any> = {}>(args: _$effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => _$effect_Cause0.YieldableError & {
91
92
  readonly _tag: "BindingValidationError";
@@ -93,7 +94,13 @@ declare const BindingValidationError_base: new <A extends Record<string, any> =
93
94
  /** Error raised when a binding exists but does not match the expected shape. */
94
95
  declare class BindingValidationError extends BindingValidationError_base<{
95
96
  readonly binding: string;
97
+ readonly expected: string;
98
+ readonly actual: string;
99
+ readonly message: string;
96
100
  }> {}
101
+ interface ValidationOptions {
102
+ readonly expected?: string;
103
+ }
97
104
  /**
98
105
  * Creates a Context tag + layer for reading and validating a Cloudflare binding.
99
106
  */
@@ -102,8 +109,8 @@ interface BindingService<Self, Id extends string, Service> extends Context.Servi
102
109
  readonly binding: string;
103
110
  readonly layer: Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
104
111
  }
105
- declare const layer$1: <Self, Resource, Service = Resource>(tag: Context.Service<Self, Service>, binding: string, isResource: (value: unknown) => value is Resource, wrap?: (resource: Resource) => Service) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
106
- declare const Service: <Self>() => <Id extends string, Resource, Service = Resource>(id: Id, binding: string, isResource: (value: unknown) => value is Resource, wrap?: (resource: Resource) => Service) => BindingService<Self, Id, Service>;
112
+ declare const layer$1: <Self, Resource, Service = Resource>(tag: Context.Service<Self, Service>, binding: string, isResource: (value: unknown) => value is Resource, wrap?: (resource: Resource) => Service, options?: ValidationOptions) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
113
+ declare const Service: <Self>() => <Id extends string, Resource, Service = Resource>(id: Id, binding: string, isResource: (value: unknown) => value is Resource, wrap?: (resource: Resource) => Service, options?: ValidationOptions) => BindingService<Self, Id, Service>;
107
114
  declare namespace Hyperdrive_d_exports {
108
115
  export { HyperdriveClient, HyperdriveDefinition, HyperdriveService, LayerOptions, Tag, TagClass, isHyperdrive, layer, make, makeClient };
109
116
  }
@@ -1,4 +1,4 @@
1
- import { i as BindingNotFoundError, n as LayerOptions, o as BindingValidationError, r as TagClass, u as WorkerEnvironment } from "./Hyperdrive-By6DrSS_.mjs";
1
+ import { i as BindingNotFoundError, n as LayerOptions, o as BindingValidationError, r as TagClass, u as WorkerEnvironment } from "./Hyperdrive-L6ldMlRF.mjs";
2
2
  import { Layer } from "effect";
3
3
  import { PgClient } from "@effect/sql-pg";
4
4
  import * as _$effect_unstable_sql_SqlClient0 from "effect/unstable/sql/SqlClient";
@@ -1,4 +1,4 @@
1
- import "./Hyperdrive-LuQaVhXU.mjs";
1
+ import "./Hyperdrive-CT6UD37x.mjs";
2
2
  import { Effect, Layer, Redacted } from "effect";
3
3
  import { PgClient } from "@effect/sql-pg";
4
4
  //#region src/HyperdrivePg.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as BindingService, c as WorkerConfig, i as BindingNotFoundError, l as WorkerEnv, o as BindingValidationError, s as Binding_d_exports, t as Hyperdrive_d_exports, u as WorkerEnvironment } from "./Hyperdrive-By6DrSS_.mjs";
1
+ import { a as BindingService, c as WorkerConfig, i as BindingNotFoundError, l as WorkerEnv, o as BindingValidationError, s as Binding_d_exports, t as Hyperdrive_d_exports, u as WorkerEnvironment } from "./Hyperdrive-L6ldMlRF.mjs";
2
2
  import { Cause, Context, Data, DateTime, Duration, Effect, Layer, Option, Schema, Scope } from "effect";
3
3
  import { D1Client } from "@effect/sql-d1";
4
4
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint, WorkflowEvent, WorkflowSleepDuration, WorkflowStep, WorkflowStepConfig, WorkflowStepContext, WorkflowStepEvent, WorkflowTimeoutDuration } from "cloudflare:workers";
@@ -184,7 +184,7 @@ declare const encodeSuccess: <const Self extends Definition$9.Any, MethodName ex
184
184
  declare const decodeSuccess: <const Self extends Definition$9.Any, MethodName extends Definition$9.MethodNames<Self>>(definition: Self, methodName: MethodName, value: unknown) => Effect.Effect<Method$5.Success<Self["methods"][MethodName]>, RpcSuccessDecodeError>;
185
185
  declare const make$9: <Id extends string, const MethodsShape extends Methods$4>(id: Id, methods: MethodsShape) => Definition$9<Id, MethodsShape>;
186
186
  declare namespace QueueBinding_d_exports {
187
- export { MessageSendRequest, QueueBindingClient, QueueBindingDefinition, QueueMetrics, QueueOperationError, QueueSendBatchOptions, QueueSendBatchResponse, QueueSendOptions, QueueSendResponse, isQueue, layer$7 as layer, makeClient$6 as makeClient };
187
+ export { MessageSendRequest, QueueBindingClient, QueueBindingDefinition, QueueMetrics, QueueOperationError, QueueProducer, QueueSendBatchOptions, QueueSendBatchResponse, QueueSendOptions, QueueSendResponse, isQueue, layer$7 as layer, makeClient$6 as makeClient };
188
188
  }
189
189
  type QueueSendOptions = globalThis.QueueSendOptions;
190
190
  type QueueSendResponse = globalThis.QueueSendResponse;
@@ -192,6 +192,7 @@ type QueueSendBatchOptions = globalThis.QueueSendBatchOptions;
192
192
  type QueueSendBatchResponse = globalThis.QueueSendBatchResponse;
193
193
  type QueueMetrics = globalThis.QueueMetrics;
194
194
  type MessageSendRequest<Body> = globalThis.MessageSendRequest<Body>;
195
+ type QueueProducer<Body> = Pick<globalThis.Queue<Body>, "send"> & Partial<Pick<globalThis.Queue<Body>, "sendBatch" | "metrics">>;
195
196
  interface QueueBindingDefinition<Message extends ServiceFreeSchema$4> {
196
197
  /** Binding name as configured in `wrangler.jsonc`. */
197
198
  readonly binding: string;
@@ -211,9 +212,10 @@ declare class QueueOperationError extends QueueOperationError_base<{
211
212
  readonly binding: string;
212
213
  readonly operation: string;
213
214
  readonly cause: unknown;
215
+ readonly message: string;
214
216
  }> {}
215
- declare const isQueue: <Body>(value: unknown) => value is globalThis.Queue<Body>;
216
- declare const makeClient$6: <Message extends ServiceFreeSchema$4>(definition: QueueBindingDefinition<Message>) => ((queue: globalThis.Queue<Schema.Codec.Encoded<Message>>) => QueueBindingClient<Message>);
217
+ declare const isQueue: <Body>(value: unknown) => value is QueueProducer<Body>;
218
+ declare const makeClient$6: <Message extends ServiceFreeSchema$4>(definition: QueueBindingDefinition<Message>) => ((queue: QueueProducer<Schema.Codec.Encoded<Message>>) => QueueBindingClient<Message>);
217
219
  declare const layer$7: <Self, Message extends ServiceFreeSchema$4>(tag: Context.Service<Self, QueueBindingClient<Message>>, definition: QueueBindingDefinition<Message>) => _$effect_Layer0.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
218
220
  declare namespace QueueDefinition_d_exports {
219
221
  export { Definition$8 as Definition, Handler$3 as Handler, LayerOptions$11 as LayerOptions, Options$7 as Options, Queue$1 as Queue, Tag$11 as Tag, TagClass$10 as TagClass, implement$7 as implement, make$8 as make };
@@ -1921,7 +1923,7 @@ declare const Tag$4: <Self>() => <Id extends string, Key, Value, EncodedValue>(i
1921
1923
  readonly value: Schema.Codec<Value, EncodedValue>;
1922
1924
  }) => TagClass$3<Self, Id, Key, Value, EncodedValue>;
1923
1925
  declare namespace R2_d_exports {
1924
- export { LayerOptions$2 as LayerOptions, R2Client, R2Definition, R2GetOptions, R2ListOptions, R2MultipartOptions, R2MultipartUploadClient, R2OperationError, R2PutOptions, R2PutValue, R2Service, R2UploadPartOptions, R2UploadPartValue, Tag$3 as Tag, TagClass$2 as TagClass, isR2Bucket, layer$1 as layer, make$2 as make, makeClient$1 as makeClient };
1926
+ export { LayerOptions$2 as LayerOptions, R2Client, R2Definition, R2GetOptions, R2ListOptions, R2MultipartOptions, R2MultipartUploadClient, R2ObjectBodyClient, R2OperationError, R2PutOptions, R2PutValue, R2Service, R2UploadPartOptions, R2UploadPartValue, Tag$3 as Tag, TagClass$2 as TagClass, isR2Bucket, layer$1 as layer, make$2 as make, makeClient$1 as makeClient };
1925
1927
  }
1926
1928
  declare const R2OperationError_base: new <A extends Record<string, any> = {}>(args: _$effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => _$effect_Cause0.YieldableError & {
1927
1929
  readonly _tag: "R2OperationError";
@@ -1944,6 +1946,14 @@ type R2MultipartOptions = globalThis.R2MultipartOptions;
1944
1946
  type R2UploadPartOptions = globalThis.R2UploadPartOptions;
1945
1947
  type R2PutValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob;
1946
1948
  type R2UploadPartValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob;
1949
+ interface R2ObjectBodyClient extends Omit<globalThis.R2ObjectBody, "arrayBuffer" | "blob" | "bytes" | "json" | "text"> {
1950
+ readonly raw: globalThis.R2ObjectBody;
1951
+ readonly arrayBuffer: Effect.Effect<ArrayBuffer, R2OperationError>;
1952
+ readonly bytes: Effect.Effect<Uint8Array, R2OperationError>;
1953
+ readonly text: Effect.Effect<string, R2OperationError>;
1954
+ readonly json: <T = unknown>() => Effect.Effect<T, R2OperationError>;
1955
+ readonly blob: Effect.Effect<Blob, R2OperationError>;
1956
+ }
1947
1957
  interface R2MultipartUploadClient {
1948
1958
  readonly raw: globalThis.R2MultipartUpload;
1949
1959
  readonly key: string;
@@ -1957,8 +1967,8 @@ interface R2Client {
1957
1967
  readonly get: {
1958
1968
  (key: string, options: R2GetOptions & {
1959
1969
  readonly onlyIf: globalThis.R2Conditional | Headers;
1960
- }): Effect.Effect<Option.Option<globalThis.R2ObjectBody | globalThis.R2Object>, R2OperationError>;
1961
- (key: string, options?: R2GetOptions): Effect.Effect<Option.Option<globalThis.R2ObjectBody>, R2OperationError>;
1970
+ }): Effect.Effect<Option.Option<R2ObjectBodyClient | globalThis.R2Object>, R2OperationError>;
1971
+ (key: string, options?: R2GetOptions): Effect.Effect<Option.Option<R2ObjectBodyClient>, R2OperationError>;
1962
1972
  };
1963
1973
  readonly put: {
1964
1974
  (key: string, value: R2PutValue, options: R2PutOptions & {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
- import { a as WorkerConfig, i as layer$8, n as Binding_exports, o as WorkerEnvironment, r as Service$2, t as Hyperdrive_exports } from "./Hyperdrive-LuQaVhXU.mjs";
2
+ import { a as WorkerConfig, i as layer$8, n as Binding_exports, o as WorkerEnvironment, r as Service$2, t as Hyperdrive_exports } from "./Hyperdrive-CT6UD37x.mjs";
3
3
  import { Cause, Clock, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Layer, ManagedRuntime, Option, Queue, Schema } from "effect";
4
4
  import { D1Client } from "@effect/sql-d1";
5
5
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint } from "cloudflare:workers";
@@ -13,6 +13,7 @@ var D1_exports = /* @__PURE__ */ __exportAll({
13
13
  make: () => make$10
14
14
  });
15
15
  const TypeId$2 = "effect-cf/D1";
16
+ const expectedD1Database = "D1 database binding with prepare(), batch(), and exec()";
16
17
  const isD1Database = (value) => {
17
18
  if (typeof value !== "object" || value === null) return false;
18
19
  const resource = value;
@@ -38,7 +39,7 @@ const make$10 = (id, definition) => Service$1()(id, definition);
38
39
  * ```
39
40
  */
40
41
  const Service$1 = () => (id, definition) => {
41
- const tag = Service$2()(id, definition.binding, isD1Database);
42
+ const tag = Service$2()(id, definition.binding, isD1Database, void 0, { expected: expectedD1Database });
42
43
  const sqlLayer = (options) => Layer.unwrap(Effect.gen(function* () {
43
44
  const db = yield* tag;
44
45
  return D1Client.layer({
@@ -60,20 +61,20 @@ var QueueBinding_exports = /* @__PURE__ */ __exportAll({
60
61
  layer: () => layer$7,
61
62
  makeClient: () => makeClient$6
62
63
  });
64
+ const expectedQueueProducer = "Queue producer binding with send(); optional sendBatch()/metrics()";
63
65
  var QueueOperationError = class extends Data.TaggedError("QueueOperationError") {};
64
- const queueError = (binding, operation, cause) => new QueueOperationError({
65
- binding,
66
- operation,
67
- cause
68
- });
69
66
  const tryQueuePromise = (binding, operation, evaluate) => Effect.tryPromise({
70
67
  try: evaluate,
71
- catch: (cause) => queueError(binding, operation, cause)
68
+ catch: (cause) => new QueueOperationError({
69
+ binding,
70
+ operation,
71
+ cause,
72
+ message: `Cloudflare queue binding "${binding}" operation "${operation}" failed`
73
+ })
72
74
  });
73
75
  const isQueue = (value) => {
74
76
  if (typeof value !== "object" || value === null) return false;
75
- const resource = value;
76
- return typeof resource.send === "function" && typeof resource.sendBatch === "function" && typeof resource.metrics === "function";
77
+ return typeof value.send === "function";
77
78
  };
78
79
  const makeClient$6 = (definition) => {
79
80
  const encodeMessage = Schema.encodeEffect(definition.message);
@@ -88,13 +89,32 @@ const makeClient$6 = (definition) => {
88
89
  ...message,
89
90
  body: yield* encodeMessage(message.body)
90
91
  });
91
- yield* tryQueuePromise(definition.binding, "sendBatch", () => queue.sendBatch(encodedMessages, options));
92
+ const sendBatch = queue.sendBatch;
93
+ if (sendBatch !== void 0) {
94
+ yield* tryQueuePromise(definition.binding, "sendBatch", () => sendBatch.call(queue, encodedMessages, options));
95
+ return;
96
+ }
97
+ yield* tryQueuePromise(definition.binding, "sendBatch", async () => {
98
+ for (const message of encodedMessages) await queue.send(message.body, {
99
+ contentType: message.contentType,
100
+ delaySeconds: message.delaySeconds ?? options?.delaySeconds
101
+ });
102
+ });
92
103
  }),
93
- metrics: () => tryQueuePromise(definition.binding, "metrics", () => queue.metrics()),
104
+ metrics: () => {
105
+ const metrics = queue.metrics;
106
+ if (metrics === void 0) return Effect.fail(new QueueOperationError({
107
+ binding: definition.binding,
108
+ operation: "metrics",
109
+ cause: /* @__PURE__ */ new Error(`Queue binding "${definition.binding}" does not provide metrics()`),
110
+ message: `Cloudflare queue binding "${definition.binding}" does not provide metrics()`
111
+ }));
112
+ return tryQueuePromise(definition.binding, "metrics", () => metrics.call(queue));
113
+ },
94
114
  unsafeRaw: Effect.succeed(queue)
95
115
  });
96
116
  };
97
- const layer$7 = (tag, definition) => layer$8(tag, definition.binding, (value) => isQueue(value), makeClient$6(definition));
117
+ const layer$7 = (tag, definition) => layer$8(tag, definition.binding, (value) => isQueue(value), makeClient$6(definition), { expected: expectedQueueProducer });
98
118
  //#endregion
99
119
  //#region src/QueueDefinition.ts
100
120
  var QueueDefinition_exports = /* @__PURE__ */ __exportAll({
@@ -378,6 +398,7 @@ var ServiceBinding_exports = /* @__PURE__ */ __exportAll({
378
398
  makeDirectMethods: () => makeDirectMethods$1
379
399
  });
380
400
  const TypeId$1 = "effect-cf/ServiceBinding";
401
+ const expectedServiceBinding = "Worker service binding with fetch()";
381
402
  /**
382
403
  * Failure raised when calling `fetch` on a service binding.
383
404
  */
@@ -436,7 +457,7 @@ const makeClient$5 = (definition) => {
436
457
  });
437
458
  };
438
459
  };
439
- const layer$6 = (tag, definition) => layer$8(tag, definition.binding, (value) => isServiceBindingClient(value), makeClient$5(definition));
460
+ const layer$6 = (tag, definition) => layer$8(tag, definition.binding, (value) => isServiceBindingClient(value), makeClient$5(definition), { expected: expectedServiceBinding });
440
461
  /**
441
462
  * Creates a typed Effect service for a Worker service binding.
442
463
  *
@@ -1003,6 +1024,7 @@ var DurableObjectNamespace_exports = /* @__PURE__ */ __exportAll({
1003
1024
  makeClient: () => makeClient$4,
1004
1025
  makeDirectMethods: () => makeDirectMethods
1005
1026
  });
1027
+ const expectedDurableObjectNamespace = "Durable Object namespace binding with getByName(), get(), idFromName(), idFromString(), newUniqueId(), and jurisdiction()";
1006
1028
  /**
1007
1029
  * Failure raised when invoking an RPC method on a Durable Object stub.
1008
1030
  */
@@ -1080,7 +1102,7 @@ const makeClient$4 = (definition) => {
1080
1102
  });
1081
1103
  };
1082
1104
  };
1083
- const layer$5 = (tag, definition) => layer$8(tag, definition.binding, (value) => isDurableObjectNamespaceClient(value), makeClient$4(definition));
1105
+ const layer$5 = (tag, definition) => layer$8(tag, definition.binding, (value) => isDurableObjectNamespaceClient(value), makeClient$4(definition), { expected: expectedDurableObjectNamespace });
1084
1106
  const makeDirectMethods = (rpcDefinition, helpers) => {
1085
1107
  const methods = {};
1086
1108
  if (rpcDefinition !== void 0) {
@@ -1752,6 +1774,7 @@ var Images_exports = /* @__PURE__ */ __exportAll({
1752
1774
  transform: () => transform
1753
1775
  });
1754
1776
  const TypeId = "effect-cf/Images/Steps";
1777
+ const expectedImagesBinding = "Images binding with info() and input()";
1755
1778
  /** Error raised when a Cloudflare Images operation fails. */
1756
1779
  var ImagesOperationError = class extends Data.TaggedError("ImagesOperationError") {};
1757
1780
  const makeSteps = (steps) => ({
@@ -1834,7 +1857,7 @@ const makeClient$3 = (definition) => (images) => {
1834
1857
  unsafeRaw: Effect.succeed(images)
1835
1858
  };
1836
1859
  };
1837
- const layer$3 = (tag, definition) => layer$8(tag, definition.binding, isImagesBinding, makeClient$3(definition));
1860
+ const layer$3 = (tag, definition) => layer$8(tag, definition.binding, isImagesBinding, makeClient$3(definition), { expected: expectedImagesBinding });
1838
1861
  const make$3 = (id) => Tag$4()(id);
1839
1862
  const Tag$4 = () => (id) => {
1840
1863
  const tag = Context.Service()(id);
@@ -1853,6 +1876,7 @@ var Kv_exports = /* @__PURE__ */ __exportAll({
1853
1876
  layer: () => layer$2,
1854
1877
  makeClient: () => makeClient$2
1855
1878
  });
1879
+ const expectedKvNamespace = "KV namespace binding with get(), put(), delete(), getWithMetadata(), and list()";
1856
1880
  /** Error raised when a KV operation fails. */
1857
1881
  var KvOperationError = class extends Data.TaggedError("KvOperationError") {};
1858
1882
  const maybeString = (value) => value == null ? Option.none() : Option.some(value);
@@ -1928,7 +1952,7 @@ const makeClient$2 = (definition) => {
1928
1952
  unsafeRaw: Effect.succeed(kv)
1929
1953
  });
1930
1954
  };
1931
- const layer$2 = (tag, definition) => layer$8(tag, definition.binding, isKvNamespace, makeClient$2(definition));
1955
+ const layer$2 = (tag, definition) => layer$8(tag, definition.binding, isKvNamespace, makeClient$2(definition), { expected: expectedKvNamespace });
1932
1956
  const makeDefinition$1 = (id, definition) => {
1933
1957
  const kvDefinition = {
1934
1958
  id,
@@ -1962,6 +1986,7 @@ var R2_exports = /* @__PURE__ */ __exportAll({
1962
1986
  make: () => make$2,
1963
1987
  makeClient: () => makeClient$1
1964
1988
  });
1989
+ const expectedR2Bucket = "R2 bucket binding with head(), get(), put(), createMultipartUpload(), resumeMultipartUpload(), delete(), and list()";
1965
1990
  /** Error raised when an R2 operation fails. */
1966
1991
  var R2OperationError = class extends Data.TaggedError("R2OperationError") {};
1967
1992
  const r2Error = (binding, operation, cause) => new R2OperationError({
@@ -1978,6 +2003,36 @@ const tryR2Sync = (binding, operation, evaluate) => Effect.try({
1978
2003
  catch: (cause) => r2Error(binding, operation, cause)
1979
2004
  });
1980
2005
  const maybe = (value) => value === null ? Option.none() : Option.some(value);
2006
+ const isR2ObjectBody = (value) => "body" in value && typeof value.arrayBuffer === "function" && typeof value.bytes === "function" && typeof value.text === "function" && typeof value.json === "function" && typeof value.blob === "function";
2007
+ const wrapObjectBody = (binding, object) => ({
2008
+ key: object.key,
2009
+ version: object.version,
2010
+ size: object.size,
2011
+ etag: object.etag,
2012
+ httpEtag: object.httpEtag,
2013
+ checksums: object.checksums,
2014
+ uploaded: object.uploaded,
2015
+ httpMetadata: object.httpMetadata,
2016
+ customMetadata: object.customMetadata,
2017
+ range: object.range,
2018
+ storageClass: object.storageClass,
2019
+ ssecKeyMd5: object.ssecKeyMd5,
2020
+ writeHttpMetadata: (headers) => object.writeHttpMetadata(headers),
2021
+ raw: object,
2022
+ body: object.body,
2023
+ get bodyUsed() {
2024
+ return object.bodyUsed;
2025
+ },
2026
+ arrayBuffer: tryR2Promise(binding, "arrayBuffer", () => object.arrayBuffer()),
2027
+ bytes: tryR2Promise(binding, "bytes", () => object.bytes()),
2028
+ text: tryR2Promise(binding, "text", () => object.text()),
2029
+ json: () => tryR2Promise(binding, "json", () => object.json()),
2030
+ blob: tryR2Promise(binding, "blob", () => object.blob())
2031
+ });
2032
+ const wrapGetResult = (binding, object) => {
2033
+ if (object === null || !isR2ObjectBody(object)) return object;
2034
+ return wrapObjectBody(binding, object);
2035
+ };
1981
2036
  const isR2Bucket = (value) => {
1982
2037
  if (typeof value !== "object" || value === null) return false;
1983
2038
  const resource = value;
@@ -1992,26 +2047,29 @@ const makeClient$1 = (definition) => {
1992
2047
  abort: tryR2Promise(definition.binding, "abortMultipartUpload", () => upload.abort()),
1993
2048
  complete: (uploadedParts) => tryR2Promise(definition.binding, "completeMultipartUpload", () => upload.complete([...uploadedParts]))
1994
2049
  });
1995
- return (bucket) => ({
1996
- definition,
1997
- head: (key) => tryR2Promise(definition.binding, "head", () => bucket.head(key)).pipe(Effect.map(maybe)),
1998
- get: ((key, options) => tryR2Promise(definition.binding, "get", () => bucket.get(key, options)).pipe(Effect.map(maybe))),
1999
- put: ((key, value, options) => tryR2Promise(definition.binding, "put", () => bucket.put(key, value, options)).pipe(Effect.map((object) => {
2000
- if (object === null) return Option.none();
2001
- if (options !== void 0 && "onlyIf" in options) return Option.some(object);
2002
- return object;
2003
- }))),
2004
- createMultipartUpload: (key, options) => tryR2Promise(definition.binding, "createMultipartUpload", () => bucket.createMultipartUpload(key, options)).pipe(Effect.map(wrapUpload)),
2005
- resumeMultipartUpload: (key, uploadId) => tryR2Sync(definition.binding, "resumeMultipartUpload", () => wrapUpload(bucket.resumeMultipartUpload(key, uploadId))),
2006
- delete: (keys) => {
2007
- const nativeKeys = typeof keys === "string" ? keys : [...keys];
2008
- return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
2009
- },
2010
- list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
2011
- unsafeRaw: Effect.succeed(bucket)
2012
- });
2050
+ return (bucket) => {
2051
+ const get = ((key, options) => tryR2Promise(definition.binding, "get", () => bucket.get(key, options)).pipe(Effect.map((object) => maybe(wrapGetResult(definition.binding, object)))));
2052
+ return {
2053
+ definition,
2054
+ head: (key) => tryR2Promise(definition.binding, "head", () => bucket.head(key)).pipe(Effect.map(maybe)),
2055
+ get,
2056
+ put: ((key, value, options) => tryR2Promise(definition.binding, "put", () => bucket.put(key, value, options)).pipe(Effect.map((object) => {
2057
+ if (object === null) return Option.none();
2058
+ if (options !== void 0 && "onlyIf" in options) return Option.some(object);
2059
+ return object;
2060
+ }))),
2061
+ createMultipartUpload: (key, options) => tryR2Promise(definition.binding, "createMultipartUpload", () => bucket.createMultipartUpload(key, options)).pipe(Effect.map(wrapUpload)),
2062
+ resumeMultipartUpload: (key, uploadId) => tryR2Sync(definition.binding, "resumeMultipartUpload", () => wrapUpload(bucket.resumeMultipartUpload(key, uploadId))),
2063
+ delete: (keys) => {
2064
+ const nativeKeys = typeof keys === "string" ? keys : [...keys];
2065
+ return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
2066
+ },
2067
+ list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
2068
+ unsafeRaw: Effect.succeed(bucket)
2069
+ };
2070
+ };
2013
2071
  };
2014
- const layer$1 = (tag, definition) => layer$8(tag, definition.binding, isR2Bucket, makeClient$1(definition));
2072
+ const layer$1 = (tag, definition) => layer$8(tag, definition.binding, isR2Bucket, makeClient$1(definition), { expected: expectedR2Bucket });
2015
2073
  const make$2 = (id) => Tag$2()(id);
2016
2074
  const Tag$2 = () => (id) => {
2017
2075
  const tag = Context.Service()(id);
@@ -2030,6 +2088,7 @@ var WorkflowBinding_exports = /* @__PURE__ */ __exportAll({
2030
2088
  layer: () => layer,
2031
2089
  makeClient: () => makeClient
2032
2090
  });
2091
+ const expectedWorkflow = "Workflow binding with create(), createBatch(), and get()";
2033
2092
  var WorkflowOperationError = class extends Data.TaggedError("WorkflowOperationError") {};
2034
2093
  var WorkflowResultDecodeError = class extends Data.TaggedError("WorkflowResultDecodeError") {};
2035
2094
  const workflowError = (binding, operation, cause) => new WorkflowOperationError({
@@ -2096,7 +2155,7 @@ const makeClient = (definition) => {
2096
2155
  unsafeRaw: Effect.succeed(workflow)
2097
2156
  });
2098
2157
  };
2099
- const layer = (tag, definition) => layer$8(tag, definition.binding, (value) => isWorkflow(value), makeClient(definition));
2158
+ const layer = (tag, definition) => layer$8(tag, definition.binding, (value) => isWorkflow(value), makeClient(definition), { expected: expectedWorkflow });
2100
2159
  //#endregion
2101
2160
  //#region src/WorkflowDefinition.ts
2102
2161
  var WorkflowDefinition_exports = /* @__PURE__ */ __exportAll({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/Binding.ts CHANGED
@@ -9,34 +9,150 @@ export const TypeId = "effect-cf/Binding" as const;
9
9
  /** Error raised when a configured binding does not exist on `env`. */
10
10
  export class BindingNotFoundError extends Data.TaggedError("BindingNotFoundError")<{
11
11
  readonly binding: string;
12
+ readonly message: string;
12
13
  }> {}
13
14
 
14
15
  /** Error raised when a binding exists but does not match the expected shape. */
15
16
  export class BindingValidationError extends Data.TaggedError("BindingValidationError")<{
16
17
  readonly binding: string;
18
+ readonly expected: string;
19
+ readonly actual: string;
20
+ readonly message: string;
17
21
  }> {}
18
22
 
23
+ export interface ValidationOptions {
24
+ readonly expected?: string;
25
+ }
26
+
27
+ const defaultExpected = "Cloudflare binding resource";
28
+
19
29
  const isPropertyTarget = (value: unknown): value is object =>
20
30
  (typeof value === "object" || typeof value === "function") && value !== null;
21
31
 
32
+ const getObjectName = (value: object | Function): string => {
33
+ const tag = (() => {
34
+ try {
35
+ return Object.prototype.toString.call(value).slice("[object ".length, -1);
36
+ } catch {
37
+ return typeof value;
38
+ }
39
+ })();
40
+ const constructorName = (() => {
41
+ try {
42
+ return "constructor" in value &&
43
+ typeof value.constructor === "function" &&
44
+ typeof value.constructor.name === "string"
45
+ ? value.constructor.name
46
+ : undefined;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ })();
51
+
52
+ if (tag !== "Object") {
53
+ return tag;
54
+ }
55
+
56
+ if (constructorName !== undefined && constructorName !== "" && constructorName !== "Object") {
57
+ return constructorName;
58
+ }
59
+
60
+ return tag;
61
+ };
62
+
63
+ const propertyNames = (value: object | Function): ReadonlyArray<string> => {
64
+ const names = new Set<string>();
65
+
66
+ for (const target of [value, Object.getPrototypeOf(value)] as const) {
67
+ if (target === null || target === Object.prototype || target === Function.prototype) {
68
+ continue;
69
+ }
70
+
71
+ try {
72
+ for (const name of Object.getOwnPropertyNames(target)) {
73
+ names.add(name);
74
+ }
75
+ } catch {
76
+ continue;
77
+ }
78
+ }
79
+
80
+ return [...names].filter((name) => name !== "constructor").sort();
81
+ };
82
+
83
+ const isMethod = (value: object | Function, name: string): boolean => {
84
+ try {
85
+ return typeof Reflect.get(value, name) === "function";
86
+ } catch {
87
+ return false;
88
+ }
89
+ };
90
+
91
+ const describeActual = (value: unknown): string => {
92
+ if (value === null) {
93
+ return "null";
94
+ }
95
+
96
+ if (typeof value !== "object" && typeof value !== "function") {
97
+ return typeof value;
98
+ }
99
+
100
+ const names = propertyNames(value);
101
+ const methods = names.filter((name) => isMethod(value, name));
102
+ const properties = names.filter((name) => !methods.includes(name));
103
+ const details = [
104
+ methods.length > 0 ? `methods ${methods.join(", ")}` : undefined,
105
+ properties.length > 0 ? `properties ${properties.join(", ")}` : undefined,
106
+ ].filter((detail) => detail !== undefined);
107
+
108
+ if (details.length === 0) {
109
+ return getObjectName(value);
110
+ }
111
+
112
+ return `${getObjectName(value)} with ${details.join("; ")}`;
113
+ };
114
+
22
115
  const getBinding = <Resource>(
23
116
  env: WorkerEnv,
24
117
  binding: string,
25
118
  isResource: (value: unknown) => value is Resource,
119
+ options?: ValidationOptions,
26
120
  ): Effect.Effect<Resource, BindingNotFoundError | BindingValidationError> =>
27
121
  Effect.gen(function* () {
28
122
  if (!isPropertyTarget(env)) {
29
- return yield* Effect.fail(new BindingValidationError({ binding }));
123
+ const actual = describeActual(env);
124
+ return yield* Effect.fail(
125
+ new BindingValidationError({
126
+ binding,
127
+ expected: "WorkerEnvironment object",
128
+ actual,
129
+ message: `Cloudflare binding "${binding}" failed validation. Expected WorkerEnvironment object; got ${actual}`,
130
+ }),
131
+ );
30
132
  }
31
133
 
32
134
  const resource = Reflect.get(env, binding);
33
135
 
34
136
  if (resource === undefined) {
35
- return yield* Effect.fail(new BindingNotFoundError({ binding }));
137
+ return yield* Effect.fail(
138
+ new BindingNotFoundError({
139
+ binding,
140
+ message: `Cloudflare binding "${binding}" was not found in WorkerEnvironment`,
141
+ }),
142
+ );
36
143
  }
37
144
 
38
145
  if (!isResource(resource)) {
39
- return yield* Effect.fail(new BindingValidationError({ binding }));
146
+ const expected = options?.expected ?? defaultExpected;
147
+ const actual = describeActual(resource);
148
+ return yield* Effect.fail(
149
+ new BindingValidationError({
150
+ binding,
151
+ expected,
152
+ actual,
153
+ message: `Cloudflare binding "${binding}" failed validation. Expected ${expected}; got ${actual}`,
154
+ }),
155
+ );
40
156
  }
41
157
 
42
158
  return resource;
@@ -64,12 +180,13 @@ export const layer = <Self, Resource, Service = Resource>(
64
180
  binding: string,
65
181
  isResource: (value: unknown) => value is Resource,
66
182
  wrap?: (resource: Resource) => Service,
183
+ options?: ValidationOptions,
67
184
  ): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment> =>
68
185
  Layer.effect(
69
186
  tag,
70
187
  Effect.gen(function* () {
71
188
  const env = yield* WorkerEnvironment;
72
- const resource = yield* getBinding(env, binding, isResource);
189
+ const resource = yield* getBinding(env, binding, isResource, options);
73
190
  return wrap === undefined ? (resource as unknown as Service) : wrap(resource);
74
191
  }),
75
192
  );
@@ -81,9 +198,10 @@ export const Service =
81
198
  binding: string,
82
199
  isResource: (value: unknown) => value is Resource,
83
200
  wrap?: (resource: Resource) => Service,
201
+ options?: ValidationOptions,
84
202
  ): BindingService<Self, Id, Service> => {
85
203
  const tag = Context.Service<Self, Service>()(id);
86
- const serviceLayer = layer(tag, binding, isResource, wrap);
204
+ const serviceLayer = layer(tag, binding, isResource, wrap, options);
87
205
 
88
206
  return Object.assign(tag, {
89
207
  [TypeId]: TypeId,
package/src/D1.ts CHANGED
@@ -4,6 +4,7 @@ import { Effect, Layer } from "effect";
4
4
  import * as Binding from "./Binding";
5
5
 
6
6
  const TypeId = "effect-cf/D1" as const;
7
+ const expectedD1Database = "D1 database binding with prepare(), batch(), and exec()";
7
8
 
8
9
  /** Typed D1 binding definition. */
9
10
  export interface D1Definition {
@@ -61,7 +62,9 @@ export const make = <Id extends string>(id: Id, definition: D1Definition) =>
61
62
  export const Service =
62
63
  <Self>() =>
63
64
  <Id extends string>(id: Id, definition: D1Definition) => {
64
- const tag = Binding.Service<Self>()(id, definition.binding, isD1Database);
65
+ const tag = Binding.Service<Self>()(id, definition.binding, isD1Database, undefined, {
66
+ expected: expectedD1Database,
67
+ });
65
68
 
66
69
  const sqlLayer = (options?: D1SqlLayerOptions) =>
67
70
  Layer.unwrap(
@@ -6,6 +6,9 @@ import type * as DurableObjectDefinition from "./DurableObjectDefinition";
6
6
  import * as RpcDefinition from "./RpcDefinition";
7
7
  import * as RpcInvocation from "./internal/RpcInvocation";
8
8
 
9
+ const expectedDurableObjectNamespace =
10
+ "Durable Object namespace binding with getByName(), get(), idFromName(), idFromString(), newUniqueId(), and jurisdiction()";
11
+
9
12
  interface DurableObjectFetcher {
10
13
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
11
14
  }
@@ -548,6 +551,7 @@ export const layer = <
548
551
  (value): value is DurableObjectNamespaceClient<Api> =>
549
552
  isDurableObjectNamespaceClient<Api>(value),
550
553
  makeClient<Api, Definition>(definition),
554
+ { expected: expectedDurableObjectNamespace },
551
555
  );
552
556
 
553
557
  export const makeDirectMethods = <
package/src/Hyperdrive.ts CHANGED
@@ -3,6 +3,8 @@ import { Context, Effect, type Layer } from "effect";
3
3
  import * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
5
5
 
6
+ const expectedHyperdrive = "Hyperdrive binding with connectionString";
7
+
6
8
  /** Typed Hyperdrive binding definition. */
7
9
  export interface HyperdriveDefinition {
8
10
  /** Binding name as configured in `wrangler.jsonc`. */
@@ -64,7 +66,10 @@ export const makeClient =
64
66
  export const layer = <Self>(
65
67
  tag: Context.Service<Self, HyperdriveClient>,
66
68
  definition: HyperdriveDefinition,
67
- ) => Binding.layer(tag, definition.binding, isHyperdrive, makeClient(definition));
69
+ ) =>
70
+ Binding.layer(tag, definition.binding, isHyperdrive, makeClient(definition), {
71
+ expected: expectedHyperdrive,
72
+ });
68
73
 
69
74
  export const make = <Id extends string>(id: Id) => Tag<HyperdriveService<Id>>()<Id>(id);
70
75
 
package/src/Images.ts CHANGED
@@ -4,6 +4,7 @@ import * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
5
5
 
6
6
  const TypeId = "effect-cf/Images/Steps" as const;
7
+ const expectedImagesBinding = "Images binding with info() and input()";
7
8
 
8
9
  /** Error raised when a Cloudflare Images operation fails. */
9
10
  export class ImagesOperationError extends Data.TaggedError("ImagesOperationError")<{
@@ -314,7 +315,10 @@ export const makeClient =
314
315
  export const layer = <Self>(
315
316
  tag: Context.Service<Self, ImagesClient>,
316
317
  definition: ImagesDefinition,
317
- ) => Binding.layer(tag, definition.binding, isImagesBinding, makeClient(definition));
318
+ ) =>
319
+ Binding.layer(tag, definition.binding, isImagesBinding, makeClient(definition), {
320
+ expected: expectedImagesBinding,
321
+ });
318
322
 
319
323
  export const make = <Id extends string>(id: Id) => Tag<ImagesService<Id>>()<Id>(id);
320
324
 
package/src/Kv.ts CHANGED
@@ -3,6 +3,9 @@ import { Context, Data, Effect, Option, Schema as S, type Layer } from "effect";
3
3
  import * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
5
5
 
6
+ const expectedKvNamespace =
7
+ "KV namespace binding with get(), put(), delete(), getWithMetadata(), and list()";
8
+
6
9
  /** Error raised when a KV operation fails. */
7
10
  export class KvOperationError extends Data.TaggedError("KvOperationError")<{
8
11
  readonly binding: string;
@@ -250,6 +253,7 @@ export const layer = <Self, Key, Value, EncodedValue>(
250
253
  definition.binding,
251
254
  isKvNamespace,
252
255
  makeClient<Key, Value, EncodedValue>(definition),
256
+ { expected: expectedKvNamespace },
253
257
  );
254
258
 
255
259
  const makeDefinition = <Id extends string, Key, Value, EncodedValue>(
@@ -10,6 +10,11 @@ export type QueueSendBatchResponse = globalThis.QueueSendBatchResponse;
10
10
  export type QueueMetrics = globalThis.QueueMetrics;
11
11
  export type MessageSendRequest<Body> = globalThis.MessageSendRequest<Body>;
12
12
 
13
+ export type QueueProducer<Body> = Pick<globalThis.Queue<Body>, "send"> &
14
+ Partial<Pick<globalThis.Queue<Body>, "sendBatch" | "metrics">>;
15
+
16
+ const expectedQueueProducer = "Queue producer binding with send(); optional sendBatch()/metrics()";
17
+
13
18
  export interface QueueBindingDefinition<Message extends RpcDefinition.ServiceFreeSchema> {
14
19
  /** Binding name as configured in `wrangler.jsonc`. */
15
20
  readonly binding: string;
@@ -34,11 +39,9 @@ export class QueueOperationError extends Data.TaggedError("QueueOperationError")
34
39
  readonly binding: string;
35
40
  readonly operation: string;
36
41
  readonly cause: unknown;
42
+ readonly message: string;
37
43
  }> {}
38
44
 
39
- const queueError = (binding: string, operation: string, cause: unknown) =>
40
- new QueueOperationError({ binding, operation, cause });
41
-
42
45
  const tryQueuePromise = <A>(
43
46
  binding: string,
44
47
  operation: string,
@@ -46,26 +49,28 @@ const tryQueuePromise = <A>(
46
49
  ): Effect.Effect<A, QueueOperationError> =>
47
50
  Effect.tryPromise({
48
51
  try: evaluate,
49
- catch: (cause) => queueError(binding, operation, cause),
52
+ catch: (cause) =>
53
+ new QueueOperationError({
54
+ binding,
55
+ operation,
56
+ cause,
57
+ message: `Cloudflare queue binding "${binding}" operation "${operation}" failed`,
58
+ }),
50
59
  });
51
60
 
52
- export const isQueue = <Body>(value: unknown): value is globalThis.Queue<Body> => {
61
+ export const isQueue = <Body>(value: unknown): value is QueueProducer<Body> => {
53
62
  if (typeof value !== "object" || value === null) {
54
63
  return false;
55
64
  }
56
65
 
57
66
  const resource = value as Record<string, unknown>;
58
67
 
59
- return (
60
- typeof resource.send === "function" &&
61
- typeof resource.sendBatch === "function" &&
62
- typeof resource.metrics === "function"
63
- );
68
+ return typeof resource.send === "function";
64
69
  };
65
70
 
66
71
  export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
67
72
  definition: QueueBindingDefinition<Message>,
68
- ): ((queue: globalThis.Queue<S.Codec.Encoded<Message>>) => QueueBindingClient<Message>) => {
73
+ ): ((queue: QueueProducer<S.Codec.Encoded<Message>>) => QueueBindingClient<Message>) => {
69
74
  type Body = S.Schema.Type<Message>;
70
75
  type EncodedBody = S.Codec.Encoded<Message>;
71
76
 
@@ -90,12 +95,41 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
90
95
  });
91
96
  }
92
97
 
93
- yield* tryQueuePromise(definition.binding, "sendBatch", () =>
94
- queue.sendBatch(encodedMessages, options),
95
- );
98
+ const sendBatch = queue.sendBatch;
99
+
100
+ if (sendBatch !== undefined) {
101
+ yield* tryQueuePromise(definition.binding, "sendBatch", () =>
102
+ sendBatch.call(queue, encodedMessages, options),
103
+ );
104
+ return;
105
+ }
106
+
107
+ yield* tryQueuePromise(definition.binding, "sendBatch", async () => {
108
+ for (const message of encodedMessages) {
109
+ await queue.send(message.body, {
110
+ contentType: message.contentType,
111
+ delaySeconds: message.delaySeconds ?? options?.delaySeconds,
112
+ });
113
+ }
114
+ });
96
115
  }),
97
- metrics: () => tryQueuePromise(definition.binding, "metrics", () => queue.metrics()),
98
- unsafeRaw: Effect.succeed(queue),
116
+ metrics: () => {
117
+ const metrics = queue.metrics;
118
+
119
+ if (metrics === undefined) {
120
+ return Effect.fail(
121
+ new QueueOperationError({
122
+ binding: definition.binding,
123
+ operation: "metrics",
124
+ cause: new Error(`Queue binding "${definition.binding}" does not provide metrics()`),
125
+ message: `Cloudflare queue binding "${definition.binding}" does not provide metrics()`,
126
+ }),
127
+ );
128
+ }
129
+
130
+ return tryQueuePromise(definition.binding, "metrics", () => metrics.call(queue));
131
+ },
132
+ unsafeRaw: Effect.succeed(queue as globalThis.Queue<EncodedBody>),
99
133
  });
100
134
  };
101
135
 
@@ -106,7 +140,8 @@ export const layer = <Self, Message extends RpcDefinition.ServiceFreeSchema>(
106
140
  Binding.layer(
107
141
  tag,
108
142
  definition.binding,
109
- (value): value is globalThis.Queue<S.Codec.Encoded<Message>> =>
143
+ (value): value is QueueProducer<S.Codec.Encoded<Message>> =>
110
144
  isQueue<S.Codec.Encoded<Message>>(value),
111
145
  makeClient(definition),
146
+ { expected: expectedQueueProducer },
112
147
  );
package/src/R2.ts CHANGED
@@ -3,6 +3,9 @@ import { Context, Data, Effect, Option, type Layer } from "effect";
3
3
  import * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
5
5
 
6
+ const expectedR2Bucket =
7
+ "R2 bucket binding with head(), get(), put(), createMultipartUpload(), resumeMultipartUpload(), delete(), and list()";
8
+
6
9
  /** Error raised when an R2 operation fails. */
7
10
  export class R2OperationError extends Data.TaggedError("R2OperationError")<{
8
11
  readonly binding: string;
@@ -24,6 +27,18 @@ export type R2UploadPartOptions = globalThis.R2UploadPartOptions;
24
27
  export type R2PutValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob;
25
28
  export type R2UploadPartValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob;
26
29
 
30
+ export interface R2ObjectBodyClient extends Omit<
31
+ globalThis.R2ObjectBody,
32
+ "arrayBuffer" | "blob" | "bytes" | "json" | "text"
33
+ > {
34
+ readonly raw: globalThis.R2ObjectBody;
35
+ readonly arrayBuffer: Effect.Effect<ArrayBuffer, R2OperationError>;
36
+ readonly bytes: Effect.Effect<Uint8Array, R2OperationError>;
37
+ readonly text: Effect.Effect<string, R2OperationError>;
38
+ readonly json: <T = unknown>() => Effect.Effect<T, R2OperationError>;
39
+ readonly blob: Effect.Effect<Blob, R2OperationError>;
40
+ }
41
+
27
42
  export interface R2MultipartUploadClient {
28
43
  readonly raw: globalThis.R2MultipartUpload;
29
44
  readonly key: string;
@@ -47,14 +62,11 @@ export interface R2Client {
47
62
  (
48
63
  key: string,
49
64
  options: R2GetOptions & { readonly onlyIf: globalThis.R2Conditional | Headers },
50
- ): Effect.Effect<
51
- Option.Option<globalThis.R2ObjectBody | globalThis.R2Object>,
52
- R2OperationError
53
- >;
65
+ ): Effect.Effect<Option.Option<R2ObjectBodyClient | globalThis.R2Object>, R2OperationError>;
54
66
  (
55
67
  key: string,
56
68
  options?: R2GetOptions,
57
- ): Effect.Effect<Option.Option<globalThis.R2ObjectBody>, R2OperationError>;
69
+ ): Effect.Effect<Option.Option<R2ObjectBodyClient>, R2OperationError>;
58
70
  };
59
71
  readonly put: {
60
72
  (
@@ -136,6 +148,53 @@ const tryR2Sync = <A>(
136
148
  const maybe = <A>(value: A | null): Option.Option<A> =>
137
149
  value === null ? Option.none() : Option.some(value);
138
150
 
151
+ const isR2ObjectBody = (
152
+ value: globalThis.R2ObjectBody | globalThis.R2Object,
153
+ ): value is globalThis.R2ObjectBody =>
154
+ "body" in value &&
155
+ typeof (value as globalThis.R2ObjectBody).arrayBuffer === "function" &&
156
+ typeof (value as globalThis.R2ObjectBody).bytes === "function" &&
157
+ typeof (value as globalThis.R2ObjectBody).text === "function" &&
158
+ typeof (value as globalThis.R2ObjectBody).json === "function" &&
159
+ typeof (value as globalThis.R2ObjectBody).blob === "function";
160
+
161
+ const wrapObjectBody = (binding: string, object: globalThis.R2ObjectBody): R2ObjectBodyClient => ({
162
+ key: object.key,
163
+ version: object.version,
164
+ size: object.size,
165
+ etag: object.etag,
166
+ httpEtag: object.httpEtag,
167
+ checksums: object.checksums,
168
+ uploaded: object.uploaded,
169
+ httpMetadata: object.httpMetadata,
170
+ customMetadata: object.customMetadata,
171
+ range: object.range,
172
+ storageClass: object.storageClass,
173
+ ssecKeyMd5: object.ssecKeyMd5,
174
+ writeHttpMetadata: (headers) => object.writeHttpMetadata(headers),
175
+ raw: object,
176
+ body: object.body,
177
+ get bodyUsed() {
178
+ return object.bodyUsed;
179
+ },
180
+ arrayBuffer: tryR2Promise(binding, "arrayBuffer", () => object.arrayBuffer()),
181
+ bytes: tryR2Promise(binding, "bytes", () => object.bytes()),
182
+ text: tryR2Promise(binding, "text", () => object.text()),
183
+ json: <T = unknown>() => tryR2Promise(binding, "json", () => object.json<T>()),
184
+ blob: tryR2Promise(binding, "blob", () => object.blob()),
185
+ });
186
+
187
+ const wrapGetResult = (
188
+ binding: string,
189
+ object: globalThis.R2ObjectBody | globalThis.R2Object | null,
190
+ ): R2ObjectBodyClient | globalThis.R2Object | null => {
191
+ if (object === null || !isR2ObjectBody(object)) {
192
+ return object;
193
+ }
194
+
195
+ return wrapObjectBody(binding, object);
196
+ };
197
+
139
198
  export const isR2Bucket = (value: unknown): value is globalThis.R2Bucket => {
140
199
  if (typeof value !== "object" || value === null) {
141
200
  return false;
@@ -172,47 +231,53 @@ export const makeClient = (
172
231
  ),
173
232
  });
174
233
 
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) =>
234
+ return (bucket) => {
235
+ const get = ((key: string, options?: R2GetOptions) =>
180
236
  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
- });
237
+ Effect.map((object) => maybe(wrapGetResult(definition.binding, object))),
238
+ )) as R2Client["get"];
239
+
240
+ return {
241
+ definition,
242
+ head: (key) =>
243
+ tryR2Promise(definition.binding, "head", () => bucket.head(key)).pipe(Effect.map(maybe)),
244
+ get,
245
+ put: ((key: string, value: R2PutValue, options?: R2PutOptions) =>
246
+ tryR2Promise(definition.binding, "put", () => bucket.put(key, value, options)).pipe(
247
+ Effect.map((object) => {
248
+ if (object === null) {
249
+ return Option.none<globalThis.R2Object>();
250
+ }
251
+
252
+ if (options !== undefined && "onlyIf" in options) {
253
+ return Option.some(object);
254
+ }
255
+
256
+ return object;
257
+ }),
258
+ )) as R2Client["put"],
259
+ createMultipartUpload: (key, options) =>
260
+ tryR2Promise(definition.binding, "createMultipartUpload", () =>
261
+ bucket.createMultipartUpload(key, options),
262
+ ).pipe(Effect.map(wrapUpload)),
263
+ resumeMultipartUpload: (key, uploadId) =>
264
+ tryR2Sync(definition.binding, "resumeMultipartUpload", () =>
265
+ wrapUpload(bucket.resumeMultipartUpload(key, uploadId)),
266
+ ),
267
+ delete: (keys) => {
268
+ const nativeKeys = typeof keys === "string" ? keys : [...keys];
269
+ return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
270
+ },
271
+ list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
272
+ unsafeRaw: Effect.succeed(bucket),
273
+ };
274
+ };
212
275
  };
213
276
 
214
277
  export const layer = <Self>(tag: Context.Service<Self, R2Client>, definition: R2Definition) =>
215
- Binding.layer(tag, definition.binding, isR2Bucket, makeClient(definition));
278
+ Binding.layer(tag, definition.binding, isR2Bucket, makeClient(definition), {
279
+ expected: expectedR2Bucket,
280
+ });
216
281
 
217
282
  export const make = <Id extends string>(id: Id) => Tag<R2Service<Id>>()<Id>(id);
218
283
 
@@ -7,6 +7,7 @@ import type * as WorkerDefinition from "./WorkerDefinition";
7
7
  import * as RpcInvocation from "./internal/RpcInvocation";
8
8
 
9
9
  const TypeId = "effect-cf/ServiceBinding" as const;
10
+ const expectedServiceBinding = "Worker service binding with fetch()";
10
11
 
11
12
  /**
12
13
  * Minimum shape for a Cloudflare service binding.
@@ -359,6 +360,7 @@ export const layer = <
359
360
  definition.binding,
360
361
  (value): value is ServiceBindingClient<Api> => isServiceBindingClient<Api>(value),
361
362
  makeClient<Api, Definition>(definition),
363
+ { expected: expectedServiceBinding },
362
364
  );
363
365
 
364
366
  /**
@@ -3,6 +3,8 @@ import { Context, Data, Effect, Option, Schema as S } from "effect";
3
3
  import * as Binding from "./Binding";
4
4
  import type * as RpcDefinition from "./RpcDefinition";
5
5
 
6
+ const expectedWorkflow = "Workflow binding with create(), createBatch(), and get()";
7
+
6
8
  export type WorkflowInstanceCreateOptions<Payload> = Omit<
7
9
  globalThis.WorkflowInstanceCreateOptions<Payload>,
8
10
  "params"
@@ -242,4 +244,5 @@ export const layer = <
242
244
  (value): value is globalThis.Workflow<S.Codec.Encoded<Payload>> =>
243
245
  isWorkflow<S.Codec.Encoded<Payload>>(value),
244
246
  makeClient(definition),
247
+ { expected: expectedWorkflow },
245
248
  );