effect-cf 0.9.1 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#32](https://github.com/danieljvdm/effect-cf/pull/32) [`4812c94`](https://github.com/danieljvdm/effect-cf/commit/4812c9457592f3416f6f303f0b8620ba52e46765) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Raise the minimum supported Effect beta to `4.0.0-beta.70` and update service tag access to use beta 70's direct yieldable tags instead of the removed `.asEffect()` helper.
8
+
9
+ ## 0.9.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#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.
14
+
3
15
  ## 0.9.1
4
16
 
5
17
  ### Patch Changes
@@ -13,7 +13,7 @@ let WorkerConfig;
13
13
  _WorkerConfig.integer = (name) => Config.int(name);
14
14
  _WorkerConfig.boolean = (name) => Config.boolean(name);
15
15
  const providerFromEnv = _WorkerConfig.providerFromEnv = (env) => ConfigProvider.fromUnknown(env);
16
- const providerWith = _WorkerConfig.providerWith = (makeProvider) => Effect.map(WorkerEnvironment.asEffect(), makeProvider);
16
+ const providerWith = _WorkerConfig.providerWith = (makeProvider) => Effect.map(WorkerEnvironment, makeProvider);
17
17
  const provider = _WorkerConfig.provider = providerWith(providerFromEnv);
18
18
  _WorkerConfig.layer = _WorkerConfig.providerLayer = ConfigProvider.layer(provider);
19
19
  _WorkerConfig.layerWith = (makeProvider) => ConfigProvider.layer(providerWith(makeProvider));
@@ -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-89hg_Fck.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 };
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-89hg_Fck.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({
@@ -2044,7 +2069,7 @@ const makeClient$1 = (definition) => {
2044
2069
  };
2045
2070
  };
2046
2071
  };
2047
- 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 });
2048
2073
  const make$2 = (id) => Tag$2()(id);
2049
2074
  const Tag$2 = () => (id) => {
2050
2075
  const tag = Context.Service()(id);
@@ -2063,6 +2088,7 @@ var WorkflowBinding_exports = /* @__PURE__ */ __exportAll({
2063
2088
  layer: () => layer,
2064
2089
  makeClient: () => makeClient
2065
2090
  });
2091
+ const expectedWorkflow = "Workflow binding with create(), createBatch(), and get()";
2066
2092
  var WorkflowOperationError = class extends Data.TaggedError("WorkflowOperationError") {};
2067
2093
  var WorkflowResultDecodeError = class extends Data.TaggedError("WorkflowResultDecodeError") {};
2068
2094
  const workflowError = (binding, operation, cause) => new WorkflowOperationError({
@@ -2129,7 +2155,7 @@ const makeClient = (definition) => {
2129
2155
  unsafeRaw: Effect.succeed(workflow)
2130
2156
  });
2131
2157
  };
2132
- 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 });
2133
2159
  //#endregion
2134
2160
  //#region src/WorkflowDefinition.ts
2135
2161
  var WorkflowDefinition_exports = /* @__PURE__ */ __exportAll({
@@ -2256,10 +2282,10 @@ const make = (layer, options) => {
2256
2282
  }
2257
2283
  return assumeEntrypointClass(EffectWorkflow);
2258
2284
  };
2259
- const step = (name, effect, config) => Effect.flatMap(WorkflowStep$1.asEffect(), (workflowStep) => workflowStep.do(name, effect, config));
2260
- const sleep = (name, duration) => Effect.flatMap(WorkflowStep$1.asEffect(), (workflowStep) => workflowStep.sleep(name, duration));
2261
- const sleepUntil = (name, timestamp) => Effect.flatMap(WorkflowStep$1.asEffect(), (workflowStep) => workflowStep.sleepUntil(name, timestamp));
2262
- const waitForEvent = (name, options) => Effect.flatMap(WorkflowStep$1.asEffect(), (workflowStep) => workflowStep.waitForEvent(name, options));
2285
+ const step = (name, effect, config) => Effect.flatMap(WorkflowStep$1, (workflowStep) => workflowStep.do(name, effect, config));
2286
+ const sleep = (name, duration) => Effect.flatMap(WorkflowStep$1, (workflowStep) => workflowStep.sleep(name, duration));
2287
+ const sleepUntil = (name, timestamp) => Effect.flatMap(WorkflowStep$1, (workflowStep) => workflowStep.sleepUntil(name, timestamp));
2288
+ const waitForEvent = (name, options) => Effect.flatMap(WorkflowStep$1, (workflowStep) => workflowStep.waitForEvent(name, options));
2263
2289
  const Tag = Tag$1;
2264
2290
  const implement = implement$1;
2265
2291
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "@cloudflare/vitest-pool-workers": "^0.15.0",
43
43
  "@cloudflare/workers-types": "^4.20260421.1",
44
44
  "@effect/sql-d1": "catalog:",
45
- "@effect/sql-pg": "4.0.0-beta.65",
45
+ "@effect/sql-pg": "catalog:",
46
46
  "@effect/vitest": "catalog:",
47
47
  "effect": "catalog:",
48
48
  "typescript": "^6.0.2",
@@ -50,9 +50,9 @@
50
50
  "vitest": "catalog:"
51
51
  },
52
52
  "peerDependencies": {
53
- "@effect/sql-d1": "^4.0.0-beta.65",
54
- "@effect/sql-pg": "^4.0.0-beta.65",
55
- "effect": "^4.0.0-beta.65"
53
+ "@effect/sql-d1": "^4.0.0-beta.70",
54
+ "@effect/sql-pg": "^4.0.0-beta.70",
55
+ "effect": "^4.0.0-beta.70"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@effect/sql-pg": {
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 = <
@@ -80,7 +80,7 @@ export namespace WorkerConfig {
80
80
  * conversion function.
81
81
  */
82
82
  export const providerWith = (makeProvider: (env: WorkerEnv) => ConfigProvider.ConfigProvider) =>
83
- Effect.map(WorkerEnvironment.asEffect(), makeProvider);
83
+ Effect.map(WorkerEnvironment, makeProvider);
84
84
 
85
85
  /** Build a `ConfigProvider` from the current `WorkerEnvironment`. */
86
86
  export const provider = providerWith(providerFromEnv);
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;
@@ -272,7 +275,9 @@ export const makeClient = (
272
275
  };
273
276
 
274
277
  export const layer = <Self>(tag: Context.Service<Self, R2Client>, definition: R2Definition) =>
275
- Binding.layer(tag, definition.binding, isR2Bucket, makeClient(definition));
278
+ Binding.layer(tag, definition.binding, isR2Bucket, makeClient(definition), {
279
+ expected: expectedR2Bucket,
280
+ });
276
281
 
277
282
  export const make = <Id extends string>(id: Id) => Tag<R2Service<Id>>()<Id>(id);
278
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
  /**
package/src/Workflow.ts CHANGED
@@ -223,7 +223,7 @@ export const step = <A, E, R>(
223
223
  effect: Effect.Effect<A, E, R>,
224
224
  config?: WorkflowStepConfig,
225
225
  ): Effect.Effect<A, unknown, WorkflowStep | Exclude<R, WorkflowStepContext>> =>
226
- Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) =>
226
+ Effect.flatMap(WorkflowStep, (workflowStep) =>
227
227
  workflowStep.do(name, effect, config),
228
228
  ) as Effect.Effect<A, unknown, WorkflowStep | Exclude<R, WorkflowStepContext>>;
229
229
 
@@ -231,15 +231,13 @@ export const sleep = (
231
231
  name: string,
232
232
  duration: WorkflowSleepDuration,
233
233
  ): Effect.Effect<void, unknown, WorkflowStep> =>
234
- Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) => workflowStep.sleep(name, duration));
234
+ Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.sleep(name, duration));
235
235
 
236
236
  export const sleepUntil = (
237
237
  name: string,
238
238
  timestamp: Date | number,
239
239
  ): Effect.Effect<void, unknown, WorkflowStep> =>
240
- Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) =>
241
- workflowStep.sleepUntil(name, timestamp),
242
- );
240
+ Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.sleepUntil(name, timestamp));
243
241
 
244
242
  export const waitForEvent = <Payload>(
245
243
  name: string,
@@ -248,9 +246,7 @@ export const waitForEvent = <Payload>(
248
246
  readonly timeout?: WorkflowTimeoutDuration | number;
249
247
  },
250
248
  ): Effect.Effect<WorkflowStepEvent<Payload>, unknown, WorkflowStep> =>
251
- Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) =>
252
- workflowStep.waitForEvent<Payload>(name, options),
253
- );
249
+ Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.waitForEvent<Payload>(name, options));
254
250
 
255
251
  export type Definition<
256
252
  Id extends string = string,
@@ -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
  );