effect-cf 0.17.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +50 -3
  3. package/dist/{Hyperdrive-C0vfrPeG.d.mts → Hyperdrive-RTjgirMV.d.mts} +7 -12
  4. package/dist/HyperdrivePg.d.mts +2 -6
  5. package/dist/HyperdrivePg.mjs +0 -1
  6. package/dist/index.d.mts +223 -118
  7. package/dist/index.mjs +587 -164
  8. package/package.json +8 -9
  9. package/src/AiGateway.ts +3 -0
  10. package/src/AnalyticsEngine.ts +6 -1
  11. package/src/Binding.ts +4 -2
  12. package/src/Cache.ts +104 -0
  13. package/src/CloudflareOtlp.ts +20 -7
  14. package/src/ContainerNamespace.ts +3 -0
  15. package/src/D1.ts +1 -0
  16. package/src/DurableObject.ts +10 -2
  17. package/src/DurableObjectAlarm.ts +19 -0
  18. package/src/DurableObjectDefinition.ts +34 -6
  19. package/src/DurableObjectNamespace.ts +4 -1
  20. package/src/DurableObjectRpcWebSocket.ts +14 -0
  21. package/src/DurableObjectSqlite.ts +1 -0
  22. package/src/DurableObjectStorage.ts +2 -0
  23. package/src/DurableObjectWebSocket.ts +4 -1
  24. package/src/Email.ts +354 -10
  25. package/src/HyperdrivePg.ts +1 -1
  26. package/src/Kv.ts +3 -1
  27. package/src/QueueBinding.ts +2 -1
  28. package/src/QueueDefinition.ts +6 -1
  29. package/src/R2.ts +1 -0
  30. package/src/RpcDefinition.ts +2 -0
  31. package/src/ServiceBinding.ts +6 -1
  32. package/src/Worker.ts +10 -2
  33. package/src/WorkerDefinition.ts +28 -6
  34. package/src/WorkflowBinding.ts +5 -2
  35. package/src/WorkflowDefinition.ts +6 -1
  36. package/src/index.ts +1 -0
  37. package/src/internal/Clock.ts +9 -0
  38. package/src/internal/RpcInvocation.ts +1 -0
  39. package/dist/Hyperdrive-89hg_Fck.mjs +0 -156
  40. /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
@@ -70,18 +70,23 @@ export const layer = (options: LayerOptions = {}) =>
70
70
 
71
71
  const reserveClientId = (socket: DurableWebSocket) => {
72
72
  const attachment = readAttachment(socket.raw, attachmentKey);
73
+
73
74
  if (attachment !== undefined) {
74
75
  nextClientId = Math.max(nextClientId, attachment + 1);
76
+
75
77
  return attachment;
76
78
  }
77
79
 
78
80
  const id = nextClientId++;
81
+
79
82
  writeAttachment(socket.raw, attachmentKey, id);
83
+
80
84
  return id;
81
85
  };
82
86
 
83
87
  const register = (socket: DurableWebSocket) => {
84
88
  const existing = connectionsBySocket.get(socket.raw);
89
+
85
90
  if (existing !== undefined) {
86
91
  return existing;
87
92
  }
@@ -95,12 +100,14 @@ export const layer = (options: LayerOptions = {}) =>
95
100
  connectionsBySocket.set(socket.raw, connection);
96
101
  connectionsById.set(connection.id, connection);
97
102
  clientIds.add(connection.id);
103
+
98
104
  return connection;
99
105
  };
100
106
 
101
107
  const unregister = (socket: DurableWebSocket) =>
102
108
  Effect.sync(() => {
103
109
  const connection = connectionsBySocket.get(socket.raw);
110
+
104
111
  if (connection === undefined) {
105
112
  return;
106
113
  }
@@ -119,11 +126,13 @@ export const layer = (options: LayerOptions = {}) =>
119
126
  Effect.sync(() => {
120
127
  try {
121
128
  const encoded = connection.parser.encode(response);
129
+
122
130
  if (encoded !== undefined) {
123
131
  connection.socket.raw.send(encoded);
124
132
  }
125
133
  } catch (cause) {
126
134
  const encoded = connection.parser.encode(RpcMessage.ResponseDefectEncoded(cause));
135
+
127
136
  if (encoded !== undefined) {
128
137
  connection.socket.raw.send(encoded);
129
138
  }
@@ -137,11 +146,13 @@ export const layer = (options: LayerOptions = {}) =>
137
146
  disconnects,
138
147
  send: (clientId, response) => {
139
148
  const connection = connectionsById.get(clientId);
149
+
140
150
  return connection === undefined ? Effect.void : send(connection, response);
141
151
  },
142
152
  end: (clientId) =>
143
153
  Effect.sync(() => {
144
154
  const connection = connectionsById.get(clientId);
155
+
145
156
  connection?.socket.raw.close();
146
157
  }),
147
158
  clientIds: Effect.sync(() => clientIds),
@@ -167,8 +178,10 @@ export const layer = (options: LayerOptions = {}) =>
167
178
  });
168
179
 
169
180
  const run = writeRequest;
181
+
170
182
  if (run === undefined) {
171
183
  yield* send(connection, RpcMessage.ResponseDefectEncoded("RPC server is not ready"));
184
+
172
185
  return;
173
186
  }
174
187
 
@@ -178,6 +191,7 @@ export const layer = (options: LayerOptions = {}) =>
178
191
  }).pipe(
179
192
  Effect.catch((error) => {
180
193
  const connection = register(socket);
194
+
181
195
  return send(connection, error);
182
196
  }),
183
197
  ),
@@ -21,6 +21,7 @@ export const layer = (options?: SqliteLayerOptions) =>
21
21
  Layer.unwrap(
22
22
  Effect.gen(function* () {
23
23
  const state = yield* DurableObjectState;
24
+
24
25
  return SqliteClient.layer({ ...options, db: state.raw.storage.sql });
25
26
  }),
26
27
  );
@@ -219,11 +219,13 @@ const schemaBackedSyncKvStorage = <Key, Value, EncodedValue>(
219
219
  Effect.gen(function* () {
220
220
  const encodedKey = yield* encodeKey(key);
221
221
  const encodedValue = yield* encodeValue(value);
222
+
222
223
  yield* tryStorageSync("kv.put", () => kv.put(encodedKey, encodedValue));
223
224
  }),
224
225
  delete: (key) =>
225
226
  Effect.gen(function* () {
226
227
  const encodedKey = yield* encodeKey(key);
228
+
227
229
  return yield* tryStorageSync("kv.delete", () => kv.delete(encodedKey));
228
230
  }),
229
231
  list: (options) =>
@@ -46,6 +46,7 @@ export const fromWebSocket = <Attachment = unknown>(
46
46
  raw: WebSocket,
47
47
  ): DurableWebSocket<Attachment> => {
48
48
  const existing = wrappers.get(raw);
49
+
49
50
  if (existing !== undefined) {
50
51
  return existing as DurableWebSocket<Attachment>;
51
52
  }
@@ -74,6 +75,7 @@ export const fromWebSocket = <Attachment = unknown>(
74
75
  };
75
76
 
76
77
  wrappers.set(raw, socket);
78
+
77
79
  return socket as DurableWebSocket<Attachment>;
78
80
  };
79
81
 
@@ -189,7 +191,8 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
189
191
  const deserialize = (socket: DurableWebSocket<unknown>) =>
190
192
  Effect.gen(function* () {
191
193
  const value = yield* socket.deserializeAttachment;
192
- if (value == null) {
194
+
195
+ if (value === null || value === undefined) {
193
196
  return Option.none<Attachment>();
194
197
  }
195
198
 
package/src/Email.ts CHANGED
@@ -11,12 +11,71 @@ import * as Binding from "./Binding";
11
11
  import type { WorkerEnvironment } from "./Environment";
12
12
 
13
13
  const expectedSendEmailBinding = "Send Email binding with send()";
14
+ const textEncoder = new TextEncoder();
15
+
16
+ /** Documented Cloudflare Email Sending limits. */
17
+ export const sendLimits = {
18
+ /** Combined `to`, `cc`, and `bcc` addresses per message. */
19
+ maxRecipients: 50,
20
+ maxAttachments: 32,
21
+ /** Total message size, including attachments. Not enforced client side. */
22
+ maxMessageBytes: 5 * 1024 * 1024,
23
+ maxHeaderNameBytes: 100,
24
+ maxHeaderValueBytes: 2048,
25
+ maxHeadersBytes: 16 * 1024,
26
+ /** Allowlisted custom headers, excluding `X-` prefixed headers. */
27
+ maxAllowlistHeaders: 20,
28
+ } as const;
29
+
30
+ /** Error codes reported by Cloudflare Email Sending. */
31
+ export const emailErrorCodes = [
32
+ "E_VALIDATION_ERROR",
33
+ "E_FIELD_MISSING",
34
+ "E_TOO_MANY_RECIPIENTS",
35
+ "E_TOO_MANY_ATTACHMENTS",
36
+ "E_SENDER_NOT_VERIFIED",
37
+ "E_RECIPIENT_NOT_ALLOWED",
38
+ "E_RECIPIENT_SUPPRESSED",
39
+ "E_SENDER_DOMAIN_NOT_AVAILABLE",
40
+ "E_CONTENT_TOO_LARGE",
41
+ "E_DELIVERY_FAILED",
42
+ "E_RATE_LIMIT_EXCEEDED",
43
+ "E_DAILY_LIMIT_EXCEEDED",
44
+ "E_INTERNAL_SERVER_ERROR",
45
+ "E_HEADER_NOT_ALLOWED",
46
+ "E_HEADER_USE_API_FIELD",
47
+ "E_HEADER_VALUE_INVALID",
48
+ "E_HEADER_VALUE_TOO_LONG",
49
+ "E_HEADER_NAME_INVALID",
50
+ "E_HEADERS_TOO_LARGE",
51
+ "E_HEADERS_TOO_MANY",
52
+ ] as const;
53
+
54
+ /** A documented Cloudflare Email Sending error code. */
55
+ export type EmailErrorCode = (typeof emailErrorCodes)[number];
14
56
 
15
57
  /** Error raised when a Cloudflare Send Email operation fails. */
16
58
  export class EmailOperationError extends Data.TaggedError("EmailOperationError")<{
17
59
  readonly binding: string;
18
60
  readonly operation: string;
19
61
  readonly cause: unknown;
62
+ /** Cloudflare error code taken from the thrown error, when present. */
63
+ readonly code?: EmailErrorCode | (string & {});
64
+ }> {}
65
+
66
+ /** A single message field that violates a documented Email Sending limit. */
67
+ export interface EmailViolation {
68
+ readonly path: string;
69
+ readonly message: string;
70
+ readonly limit?: number;
71
+ readonly actual?: number;
72
+ }
73
+
74
+ /** Error raised when a message violates documented Cloudflare Email Sending limits. */
75
+ export class EmailValidationError extends Data.TaggedError("EmailValidationError")<{
76
+ readonly binding: string;
77
+ readonly operation: string;
78
+ readonly violations: ReadonlyArray<EmailViolation>;
20
79
  }> {}
21
80
 
22
81
  /** Typed Cloudflare Send Email binding definition. */
@@ -32,6 +91,13 @@ export type EmailMessageBuilder = Parameters<CloudflareSendEmail["send"]>[0];
32
91
  export type EmailSendInput = EmailMessage | EmailMessageBuilder;
33
92
  export type EmailSendResult = CloudflareEmailSendResult;
34
93
  export type EmailBinding = CloudflareSendEmail;
94
+ export type EmailRecipients = string | EmailAddress | ReadonlyArray<string | EmailAddress>;
95
+ export type EmailSendError = EmailOperationError | EmailValidationError;
96
+
97
+ export interface EmailSendOptions {
98
+ /** Validates builder messages against documented limits. Defaults to `true`. */
99
+ readonly validate?: boolean;
100
+ }
35
101
 
36
102
  interface EmailRuntimeBinding {
37
103
  readonly send: (message: EmailSendInput) => Promise<EmailSendResult>;
@@ -39,8 +105,8 @@ interface EmailRuntimeBinding {
39
105
 
40
106
  export interface EmailClient {
41
107
  readonly send: {
42
- (message: EmailMessage): Effect.Effect<EmailSendResult, EmailOperationError>;
43
- (builder: EmailMessageBuilder): Effect.Effect<EmailSendResult, EmailOperationError>;
108
+ (message: EmailMessage): Effect.Effect<EmailSendResult, EmailSendError>;
109
+ (builder: EmailMessageBuilder): Effect.Effect<EmailSendResult, EmailSendError>;
44
110
  };
45
111
  readonly unsafeRaw: Effect.Effect<EmailBinding>;
46
112
  readonly definition: EmailDefinition;
@@ -57,6 +123,7 @@ export interface EmailService<Id extends string> {
57
123
 
58
124
  export type LayerOptions = {
59
125
  readonly binding: string;
126
+ readonly send?: EmailSendOptions;
60
127
  };
61
128
 
62
129
  export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
@@ -74,8 +141,24 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
74
141
  >;
75
142
  }
76
143
 
144
+ const emailErrorCode = (cause: unknown): EmailOperationError["code"] => {
145
+ if (typeof cause !== "object" || cause === null) {
146
+ return undefined;
147
+ }
148
+
149
+ const code = Reflect.get(cause, "code");
150
+
151
+ return typeof code === "string" ? code : undefined;
152
+ };
153
+
77
154
  const emailError = (binding: string, operation: string, cause: unknown) =>
78
- new EmailOperationError({ binding, operation, cause });
155
+ new EmailOperationError({ binding, operation, cause, code: emailErrorCode(cause) });
156
+
157
+ const emailValidationError = (
158
+ binding: string,
159
+ operation: string,
160
+ violations: ReadonlyArray<EmailViolation>,
161
+ ) => new EmailValidationError({ binding, operation, violations });
79
162
 
80
163
  const tryEmailPromise = <A>(
81
164
  binding: string,
@@ -87,6 +170,260 @@ const tryEmailPromise = <A>(
87
170
  catch: (cause) => emailError(binding, operation, cause),
88
171
  });
89
172
 
173
+ const builderFields = [
174
+ "subject",
175
+ "html",
176
+ "text",
177
+ "cc",
178
+ "bcc",
179
+ "replyTo",
180
+ "attachments",
181
+ "headers",
182
+ ] as const;
183
+
184
+ /**
185
+ * Distinguishes structured Email Sending messages from raw MIME `EmailMessage`
186
+ * values, which only carry envelope `from` and `to` addresses.
187
+ */
188
+ export const isEmailMessageBuilder = (message: EmailSendInput): message is EmailMessageBuilder => {
189
+ if (typeof message !== "object" || message === null) {
190
+ return false;
191
+ }
192
+
193
+ return builderFields.some((field) => Reflect.get(message, field) !== undefined);
194
+ };
195
+
196
+ const byteLength = (value: string) => textEncoder.encode(value).byteLength;
197
+
198
+ const limitViolation = (
199
+ path: string,
200
+ label: string,
201
+ actual: number,
202
+ limit: number,
203
+ ): EmailViolation => ({
204
+ path,
205
+ message: `${label} exceeds Cloudflare Email Sending limit`,
206
+ actual,
207
+ limit,
208
+ });
209
+
210
+ const addressList = (value: EmailRecipients | undefined): ReadonlyArray<string | EmailAddress> => {
211
+ if (value === undefined) {
212
+ return [];
213
+ }
214
+
215
+ return Array.isArray(value) ? value : [value as string | EmailAddress];
216
+ };
217
+
218
+ const addressViolations = (path: string, value: unknown): ReadonlyArray<EmailViolation> => {
219
+ if (typeof value === "string") {
220
+ return value.includes("@") ? [] : [{ path, message: "Email address must contain an @ symbol" }];
221
+ }
222
+
223
+ if (typeof value !== "object" || value === null) {
224
+ return [{ path, message: "Email address must be a string or { name, email } object" }];
225
+ }
226
+
227
+ const email = Reflect.get(value, "email");
228
+
229
+ if (typeof email !== "string" || !email.includes("@")) {
230
+ return [{ path: `${path}.email`, message: "Email address must contain an @ symbol" }];
231
+ }
232
+
233
+ return [];
234
+ };
235
+
236
+ const recipientViolations = (
237
+ field: "to" | "cc" | "bcc",
238
+ value: EmailRecipients | undefined,
239
+ ): ReadonlyArray<EmailViolation> =>
240
+ addressList(value).flatMap((address, index) => addressViolations(`${field}[${index}]`, address));
241
+
242
+ const attachmentViolations = (
243
+ attachments: ReadonlyArray<EmailAttachment>,
244
+ ): ReadonlyArray<EmailViolation> => {
245
+ const violations: Array<EmailViolation> = [];
246
+
247
+ if (attachments.length > sendLimits.maxAttachments) {
248
+ violations.push(
249
+ limitViolation("attachments", "attachments", attachments.length, sendLimits.maxAttachments),
250
+ );
251
+ }
252
+
253
+ for (let index = 0; index < attachments.length; index++) {
254
+ const attachment = attachments[index];
255
+ const path = `attachments[${index}]`;
256
+
257
+ if (typeof attachment !== "object" || attachment === null) {
258
+ violations.push({ path, message: "Attachment must be an object" });
259
+ continue;
260
+ }
261
+
262
+ if (typeof attachment.filename !== "string" || attachment.filename === "") {
263
+ violations.push({ path: `${path}.filename`, message: "Attachment filename is required" });
264
+ }
265
+
266
+ if (typeof attachment.type !== "string" || attachment.type === "") {
267
+ violations.push({ path: `${path}.type`, message: "Attachment MIME type is required" });
268
+ }
269
+
270
+ if (attachment.content === undefined || attachment.content === null) {
271
+ violations.push({ path: `${path}.content`, message: "Attachment content is required" });
272
+ }
273
+
274
+ if (attachment.disposition === "inline" && typeof attachment.contentId !== "string") {
275
+ violations.push({
276
+ path: `${path}.contentId`,
277
+ message: "Inline attachments require a contentId",
278
+ });
279
+ }
280
+ }
281
+
282
+ return violations;
283
+ };
284
+
285
+ const headerViolations = (headers: Record<string, string>): ReadonlyArray<EmailViolation> => {
286
+ const violations: Array<EmailViolation> = [];
287
+ const entries = Object.entries(headers);
288
+ let totalBytes = 0;
289
+ let allowlistHeaders = 0;
290
+
291
+ for (const [name, value] of entries) {
292
+ const path = `headers["${name}"]`;
293
+ const nameBytes = byteLength(name);
294
+
295
+ if (nameBytes > sendLimits.maxHeaderNameBytes) {
296
+ violations.push(
297
+ limitViolation(path, "header name bytes", nameBytes, sendLimits.maxHeaderNameBytes),
298
+ );
299
+ }
300
+
301
+ if (typeof value !== "string") {
302
+ violations.push({ path, message: "Header value must be a string" });
303
+ continue;
304
+ }
305
+
306
+ const valueBytes = byteLength(value);
307
+
308
+ if (valueBytes > sendLimits.maxHeaderValueBytes) {
309
+ violations.push(
310
+ limitViolation(path, "header value bytes", valueBytes, sendLimits.maxHeaderValueBytes),
311
+ );
312
+ }
313
+
314
+ totalBytes += nameBytes + valueBytes;
315
+
316
+ if (!name.toLowerCase().startsWith("x-")) {
317
+ allowlistHeaders += 1;
318
+ }
319
+ }
320
+
321
+ if (totalBytes > sendLimits.maxHeadersBytes) {
322
+ violations.push(
323
+ limitViolation("headers", "header bytes", totalBytes, sendLimits.maxHeadersBytes),
324
+ );
325
+ }
326
+
327
+ if (allowlistHeaders > sendLimits.maxAllowlistHeaders) {
328
+ violations.push(
329
+ limitViolation(
330
+ "headers",
331
+ "allowlisted custom headers",
332
+ allowlistHeaders,
333
+ sendLimits.maxAllowlistHeaders,
334
+ ),
335
+ );
336
+ }
337
+
338
+ return violations;
339
+ };
340
+
341
+ /**
342
+ * Validates a structured message against documented Cloudflare Email Sending
343
+ * limits and returns every violation found. Total message size is not checked
344
+ * because the encoded MIME size is only known once Cloudflare composes it.
345
+ */
346
+ export const validateMessage = (message: EmailMessageBuilder): ReadonlyArray<EmailViolation> => {
347
+ const violations: Array<EmailViolation> = [];
348
+ const fields = message as {
349
+ readonly to?: EmailRecipients;
350
+ readonly cc?: EmailRecipients;
351
+ readonly bcc?: EmailRecipients;
352
+ readonly from?: unknown;
353
+ readonly replyTo?: unknown;
354
+ readonly subject?: unknown;
355
+ readonly text?: unknown;
356
+ readonly html?: unknown;
357
+ readonly attachments?: ReadonlyArray<EmailAttachment>;
358
+ readonly headers?: Record<string, string>;
359
+ };
360
+
361
+ if (fields.from === undefined) {
362
+ violations.push({ path: "from", message: "A from address is required" });
363
+ } else {
364
+ violations.push(...addressViolations("from", fields.from));
365
+ }
366
+
367
+ if (fields.replyTo !== undefined) {
368
+ violations.push(...addressViolations("replyTo", fields.replyTo));
369
+ }
370
+
371
+ if (typeof fields.subject !== "string" || fields.subject === "") {
372
+ violations.push({ path: "subject", message: "A subject is required" });
373
+ }
374
+
375
+ if (typeof fields.text !== "string" && typeof fields.html !== "string") {
376
+ violations.push({ path: "$", message: "A message requires either text or html content" });
377
+ }
378
+
379
+ const recipients =
380
+ addressList(fields.to).length + addressList(fields.cc).length + addressList(fields.bcc).length;
381
+
382
+ if (recipients === 0) {
383
+ violations.push({
384
+ path: "$",
385
+ message: "A message requires at least one to, cc, or bcc address",
386
+ });
387
+ }
388
+
389
+ if (recipients > sendLimits.maxRecipients) {
390
+ violations.push(
391
+ limitViolation(
392
+ "$",
393
+ "combined to, cc, and bcc addresses",
394
+ recipients,
395
+ sendLimits.maxRecipients,
396
+ ),
397
+ );
398
+ }
399
+
400
+ violations.push(...recipientViolations("to", fields.to));
401
+ violations.push(...recipientViolations("cc", fields.cc));
402
+ violations.push(...recipientViolations("bcc", fields.bcc));
403
+
404
+ if (fields.attachments !== undefined) {
405
+ violations.push(...attachmentViolations(fields.attachments));
406
+ }
407
+
408
+ if (fields.headers !== undefined) {
409
+ violations.push(...headerViolations(fields.headers));
410
+ }
411
+
412
+ return violations;
413
+ };
414
+
415
+ const validateSendInput = (
416
+ binding: string,
417
+ operation: string,
418
+ message: EmailMessageBuilder,
419
+ ): Effect.Effect<EmailMessageBuilder, EmailValidationError> => {
420
+ const violations = validateMessage(message);
421
+
422
+ return violations.length > 0
423
+ ? Effect.fail(emailValidationError(binding, operation, violations))
424
+ : Effect.succeed(message);
425
+ };
426
+
90
427
  export const isEmailBinding = (value: unknown): value is EmailBinding => {
91
428
  if (typeof value !== "object" || value === null) {
92
429
  return false;
@@ -98,13 +435,20 @@ export const isEmailBinding = (value: unknown): value is EmailBinding => {
98
435
  };
99
436
 
100
437
  export const makeClient =
101
- (definition: EmailDefinition) =>
438
+ (definition: EmailDefinition, options?: EmailSendOptions) =>
102
439
  (email: EmailBinding): EmailClient => {
103
440
  const runtime = email as EmailRuntimeBinding;
104
- const send = ((message: EmailSendInput) =>
105
- tryEmailPromise(definition.binding, "send", () =>
106
- runtime.send(message),
107
- )) as EmailClient["send"];
441
+ const validate = options?.validate ?? true;
442
+ const send = ((message: EmailSendInput) => {
443
+ const dispatch = () =>
444
+ tryEmailPromise(definition.binding, "send", () => runtime.send(message));
445
+
446
+ if (!validate || !isEmailMessageBuilder(message)) {
447
+ return dispatch();
448
+ }
449
+
450
+ return validateSendInput(definition.binding, "send", message).pipe(Effect.flatMap(dispatch));
451
+ }) as EmailClient["send"];
108
452
 
109
453
  return {
110
454
  definition,
@@ -113,8 +457,8 @@ export const makeClient =
113
457
  };
114
458
  };
115
459
 
116
- export const layer = <Self>(tag: Context.Service<Self, EmailClient>, definition: EmailDefinition) =>
117
- Binding.layer(tag, definition.binding, isEmailBinding, makeClient(definition), {
460
+ export const layer = <Self>(tag: Context.Service<Self, EmailClient>, definition: LayerOptions) =>
461
+ Binding.layer(tag, definition.binding, isEmailBinding, makeClient(definition, definition.send), {
118
462
  expected: expectedSendEmailBinding,
119
463
  });
120
464
 
@@ -1,7 +1,7 @@
1
1
  import { PgClient } from "@effect/sql-pg";
2
2
  import { Effect, Layer, Redacted } from "effect";
3
3
 
4
- import * as Hyperdrive from "./Hyperdrive";
4
+ import type * as Hyperdrive from "./Hyperdrive";
5
5
 
6
6
  export type PgLayerOptions = Omit<
7
7
  PgClient.PgClientConfig & { readonly acquireForStream?: boolean },
package/src/Kv.ts CHANGED
@@ -124,7 +124,7 @@ export interface TagClass<
124
124
  }
125
125
 
126
126
  const maybeString = (value: string | null | undefined): Option.Option<string> =>
127
- value == null ? Option.none() : Option.some(value);
127
+ value === null || value === undefined ? Option.none() : Option.some(value);
128
128
 
129
129
  const maybeNumber = (value: number | undefined): Option.Option<number> =>
130
130
  value === undefined ? Option.none() : Option.some(value);
@@ -171,6 +171,7 @@ export const makeClient = <Key, Value, EncodedValue>(
171
171
  put: Effect.fnUntraced(function* (key: Key, value: Value, options?: KvPutOptions) {
172
172
  const keyEncoded = yield* encodeKey(key);
173
173
  const valueEncoded = yield* encodeValue(value);
174
+
174
175
  yield* tryKvPromise(definition.binding, "put", () =>
175
176
  kv.put(keyEncoded, valueEncoded, options),
176
177
  );
@@ -242,6 +243,7 @@ export const makeClient = <Key, Value, EncodedValue>(
242
243
  }),
243
244
  remove: Effect.fnUntraced(function* (key: Key) {
244
245
  const keyEncoded = yield* encodeKey(key);
246
+
245
247
  yield* tryKvPromise(definition.binding, "delete", () => kv.delete(keyEncoded));
246
248
  }),
247
249
  unsafeRaw: Effect.succeed(kv),
@@ -7,7 +7,7 @@ import type {
7
7
  QueueSendOptions as CloudflareQueueSendOptions,
8
8
  QueueSendResponse as CloudflareQueueSendResponse,
9
9
  } from "@cloudflare/workers-types";
10
- import { Context, Data, Effect, Schema as S } from "effect";
10
+ import { type Context, Data, Effect, Schema as S } from "effect";
11
11
 
12
12
  import * as Binding from "./Binding";
13
13
  import type * as RpcDefinition from "./RpcDefinition";
@@ -110,6 +110,7 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
110
110
  yield* tryQueuePromise(definition.binding, "sendBatch", () =>
111
111
  sendBatch.call(queue, encodedMessages, options),
112
112
  );
113
+
113
114
  return;
114
115
  }
115
116
 
@@ -1,6 +1,6 @@
1
1
  import { Context, Effect, Schema as S, type Layer, type Scope } from "effect";
2
2
 
3
- import * as Binding from "./Binding";
3
+ import type * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
5
5
  import * as QueueEntrypoint from "./Queue";
6
6
  import * as QueueBinding from "./QueueBinding";
@@ -121,6 +121,7 @@ export const Tag =
121
121
  options?: QueueBinding.QueueSendOptions,
122
122
  ) {
123
123
  const queue = yield* tag;
124
+
124
125
  yield* queue.send(message, options);
125
126
  });
126
127
 
@@ -129,16 +130,19 @@ export const Tag =
129
130
  options?: QueueBinding.QueueSendBatchOptions,
130
131
  ) {
131
132
  const queue = yield* tag;
133
+
132
134
  yield* queue.sendBatch(messages, options);
133
135
  });
134
136
 
135
137
  const metrics = Effect.fnUntraced(function* () {
136
138
  const queue = yield* tag;
139
+
137
140
  return yield* queue.metrics();
138
141
  });
139
142
 
140
143
  const unsafeRaw = Effect.fnUntraced(function* () {
141
144
  const queue = yield* tag;
145
+
142
146
  return yield* queue.unsafeRaw;
143
147
  });
144
148
 
@@ -165,6 +169,7 @@ const wrapHandler = <ROut, const Self extends Definition.Any>(
165
169
  return (batch) =>
166
170
  Effect.gen(function* () {
167
171
  const decoded = yield* QueueEntrypoint.decodeBatch(batch.raw, decodeBody);
172
+
168
173
  yield* handler(decoded);
169
174
  });
170
175
  };
package/src/R2.ts CHANGED
@@ -280,6 +280,7 @@ export const makeClient = (
280
280
  ),
281
281
  delete: (keys) => {
282
282
  const nativeKeys = typeof keys === "string" ? keys : [...keys];
283
+
283
284
  return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
284
285
  },
285
286
  list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
@@ -218,6 +218,7 @@ export const decodeArgs = <
218
218
 
219
219
  for (let index = 0; index < methodDefinition.args.length; index++) {
220
220
  const schema = methodDefinition.args[index];
221
+
221
222
  decoded.push(
222
223
  yield* (S.decodeUnknownEffect(schema)(args[index]) as Effect.Effect<unknown, unknown>).pipe(
223
224
  Effect.mapError(
@@ -265,6 +266,7 @@ export const encodeArgs = <
265
266
 
266
267
  for (let index = 0; index < methodDefinition.args.length; index++) {
267
268
  const schema = methodDefinition.args[index];
269
+
268
270
  encoded.push(
269
271
  yield* (S.encodeEffect(schema)(args[index]) as Effect.Effect<unknown, unknown>).pipe(
270
272
  Effect.mapError(