effect-cf 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +2 -2
  3. package/dist/{Hyperdrive-RTjgirMV.d.mts → Hyperdrive-COyg7R26.d.mts} +40 -13
  4. package/dist/HyperdrivePg.d.mts +13 -5
  5. package/dist/HyperdrivePg.mjs +12 -7
  6. package/dist/index.d.mts +708 -549
  7. package/dist/index.mjs +946 -589
  8. package/package.json +10 -5
  9. package/src/AiGateway.ts +82 -47
  10. package/src/AnalyticsEngine.ts +74 -58
  11. package/src/Binding.ts +66 -13
  12. package/src/BrowserRendering.ts +30 -7
  13. package/src/Cache.ts +32 -8
  14. package/src/CloudflareOtlp.ts +32 -9
  15. package/src/ContainerNamespace.ts +87 -55
  16. package/src/D1.ts +13 -3
  17. package/src/DurableObject.ts +27 -164
  18. package/src/DurableObjectAlarm.ts +30 -9
  19. package/src/DurableObjectDefinition.ts +58 -54
  20. package/src/DurableObjectNamespace.ts +99 -96
  21. package/src/DurableObjectRpcWebSocket.ts +14 -6
  22. package/src/DurableObjectSqlite.ts +6 -3
  23. package/src/DurableObjectState.ts +6 -9
  24. package/src/DurableObjectStorage.ts +7 -1
  25. package/src/DurableObjectWebSocket.ts +51 -41
  26. package/src/Email.ts +40 -13
  27. package/src/Environment.ts +26 -6
  28. package/src/Hyperdrive.ts +2 -2
  29. package/src/HyperdrivePg.ts +25 -13
  30. package/src/Images.ts +77 -82
  31. package/src/Kv.ts +37 -27
  32. package/src/Queue.ts +6 -1
  33. package/src/QueueBinding.ts +18 -21
  34. package/src/QueueDefinition.ts +15 -14
  35. package/src/R2.ts +107 -29
  36. package/src/Rpc.ts +3 -1
  37. package/src/RpcDefinition.ts +177 -133
  38. package/src/ServiceBinding.ts +112 -108
  39. package/src/Vectorize.ts +59 -14
  40. package/src/Worker.ts +205 -198
  41. package/src/WorkerDefinition.ts +5 -10
  42. package/src/WorkersAi.ts +27 -17
  43. package/src/Workflow.ts +54 -49
  44. package/src/WorkflowBinding.ts +19 -11
  45. package/src/WorkflowDefinition.ts +24 -27
  46. package/src/index.ts +4 -3
  47. package/src/internal/Entrypoint.ts +3 -1
  48. package/src/internal/ErrorMessage.ts +24 -0
  49. package/src/internal/RpcInvocation.ts +11 -8
  50. package/src/internal/Runtime.ts +52 -0
  51. package/src/internal/WorkerContext.ts +14 -2
  52. package/src/Entry.ts +0 -259
  53. package/src/internal/Clock.ts +0 -37
@@ -1,9 +1,11 @@
1
- import { Data, Effect, type Context, type Scope } from "effect";
1
+ import { Data, Effect, type Context, type Layer, type Scope } from "effect";
2
2
 
3
3
  import * as Binding from "./Binding";
4
+ import type { WorkerEnvironment } from "./Environment";
4
5
  import * as CloudflareRpc from "./Rpc";
5
6
  import type * as DurableObjectDefinition from "./DurableObjectDefinition";
6
7
  import * as RpcDefinition from "./RpcDefinition";
8
+ import * as ErrorMessage from "./internal/ErrorMessage";
7
9
  import * as RpcInvocation from "./internal/RpcInvocation";
8
10
 
9
11
  const expectedDurableObjectNamespace =
@@ -86,7 +88,11 @@ export class DurableObjectRpcError extends Data.TaggedError("DurableObjectRpcErr
86
88
  readonly binding: string;
87
89
  readonly method: string;
88
90
  readonly cause: unknown;
89
- }> {}
91
+ }> {
92
+ override get message(): string {
93
+ return `Durable Object RPC method "${this.method}" failed for binding "${this.binding}": ${ErrorMessage.causeMessage(this.cause)}`;
94
+ }
95
+ }
90
96
 
91
97
  /**
92
98
  * Failure raised when forwarding a request to a Durable Object stub.
@@ -94,7 +100,11 @@ export class DurableObjectRpcError extends Data.TaggedError("DurableObjectRpcErr
94
100
  export class DurableObjectFetchError extends Data.TaggedError("DurableObjectFetchError")<{
95
101
  readonly binding: string;
96
102
  readonly cause: unknown;
97
- }> {}
103
+ }> {
104
+ override get message(): string {
105
+ return `Durable Object fetch failed for binding "${this.binding}": ${ErrorMessage.causeMessage(this.cause)}`;
106
+ }
107
+ }
98
108
 
99
109
  type StubMethodKey<Api> = RpcInvocation.AsyncMethodKey<Api>;
100
110
  type StubMethodArgs<Api, Method extends keyof Api> = RpcInvocation.AsyncMethodArgs<Api, Method>;
@@ -316,7 +326,7 @@ export type DurableObjectNamespaceEffectClient<
316
326
  * Prefer the typed helpers above unless Cloudflare exposes a platform feature
317
327
  * that is not wrapped by effect-cf yet.
318
328
  */
319
- readonly unsafeRaw: Effect.Effect<DurableObjectNamespaceClient<Api>>;
329
+ readonly rawUnsafe: Effect.Effect<DurableObjectNamespaceClient<Api>>;
320
330
  };
321
331
 
322
332
  export type DurableObjectNamespaceStaticClient<
@@ -360,7 +370,7 @@ export type DurableObjectNamespaceStaticClient<
360
370
  method: Method,
361
371
  ...args: StubMethodArgs<Api, Method>
362
372
  ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, unknown, Scope.Scope | R>;
363
- readonly unsafeRaw: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
373
+ readonly rawUnsafe: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
364
374
  };
365
375
 
366
376
  const hasFunction = (value: object, key: string): boolean =>
@@ -416,104 +426,93 @@ export const makeClient = <
416
426
  catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
417
427
  });
418
428
 
419
- const rpc = <Method extends StubMethodKey<Api>>(
420
- stub: StubClient,
421
- method: Method,
422
- ...args: StubMethodArgs<Api, Method>
423
- ) =>
424
- Effect.gen(function* () {
425
- const methodName = String(method);
426
- const encodedArgs =
427
- definition.definition === undefined
428
- ? args
429
- : yield* RpcDefinition.encodeArgs(
430
- definition.definition,
431
- methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
432
- args as never,
433
- ).pipe(
434
- Effect.mapError(
435
- (cause) =>
436
- new DurableObjectRpcError({
437
- binding: definition.binding,
438
- method: methodName,
439
- cause,
440
- }),
441
- ),
442
- );
443
-
444
- return yield* RpcInvocation.invokeRpcMethod(
445
- stub,
446
- method,
447
- encodedArgs as StubMethodArgs<Api, Method>,
429
+ const rpc = Effect.fn("DurableObjectNamespace.rpc")(function* <
430
+ Method extends StubMethodKey<Api>,
431
+ >(stub: StubClient, method: Method, ...args: StubMethodArgs<Api, Method>) {
432
+ const methodName = String(method);
433
+ const encodedArgs =
434
+ definition.definition === undefined
435
+ ? args
436
+ : yield* RpcDefinition.encodeArgs(
437
+ definition.definition,
438
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
439
+ args as never,
440
+ ).pipe(
441
+ Effect.mapError(
442
+ (cause) =>
443
+ new DurableObjectRpcError({
444
+ binding: definition.binding,
445
+ method: methodName,
446
+ cause,
447
+ }),
448
+ ),
449
+ );
450
+
451
+ return yield* RpcInvocation.invokeRpcMethod(
452
+ stub,
453
+ method,
454
+ encodedArgs as StubMethodArgs<Api, Method>,
455
+ (cause) =>
456
+ new DurableObjectRpcError({
457
+ binding: definition.binding,
458
+ method: methodName,
459
+ cause,
460
+ }),
461
+ );
462
+ });
463
+
464
+ const decodeSuccess = Effect.fn("DurableObjectNamespace.decodeSuccess")(function* <
465
+ Method extends StubMethodKey<Api>,
466
+ >(methodName: string, value: Awaited<StubMethodCloudflareReturn<Api, Method>>) {
467
+ if (definition.definition === undefined) {
468
+ return value as StubMethodSuccess<Api, Method>;
469
+ }
470
+
471
+ const decoded = yield* RpcDefinition.decodeSuccess(
472
+ definition.definition,
473
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
474
+ value,
475
+ ).pipe(
476
+ Effect.mapError(
448
477
  (cause) =>
449
478
  new DurableObjectRpcError({
450
479
  binding: definition.binding,
451
480
  method: methodName,
452
481
  cause,
453
482
  }),
454
- );
455
- });
483
+ ),
484
+ );
456
485
 
457
- const decodeSuccess = <Method extends StubMethodKey<Api>>(
458
- methodName: string,
459
- value: Awaited<StubMethodCloudflareReturn<Api, Method>>,
460
- ) =>
461
- Effect.gen(function* () {
462
- if (definition.definition === undefined) {
463
- return value as StubMethodSuccess<Api, Method>;
464
- }
465
-
466
- const decoded = yield* RpcDefinition.decodeSuccess(
467
- definition.definition,
468
- methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
469
- value,
470
- ).pipe(
471
- Effect.mapError(
472
- (cause) =>
473
- new DurableObjectRpcError({
474
- binding: definition.binding,
475
- method: methodName,
476
- cause,
477
- }),
478
- ),
479
- );
480
-
481
- return decoded as StubMethodSuccess<Api, Method>;
482
- });
486
+ return decoded as StubMethodSuccess<Api, Method>;
487
+ });
483
488
 
484
- const call = <Method extends StubMethodKey<Api>>(
485
- stub: StubClient,
486
- method: Method,
487
- ...args: StubMethodArgs<Api, Method>
488
- ) =>
489
- Effect.gen(function* () {
490
- const methodName = String(method);
491
- const value = yield* CloudflareRpc.resolve(yield* rpc(stub, method, ...args)).pipe(
492
- Effect.mapError(
493
- (cause) =>
494
- new DurableObjectRpcError({
495
- binding: definition.binding,
496
- method: methodName,
497
- cause,
498
- }),
499
- ),
500
- );
501
-
502
- return yield* decodeSuccess<Method>(methodName, value);
503
- });
489
+ const call = Effect.fn("DurableObjectNamespace.call")(function* <
490
+ Method extends StubMethodKey<Api>,
491
+ >(stub: StubClient, method: Method, ...args: StubMethodArgs<Api, Method>) {
492
+ const methodName = String(method);
493
+ const value = yield* CloudflareRpc.resolve(yield* rpc(stub, method, ...args)).pipe(
494
+ Effect.mapError(
495
+ (cause) =>
496
+ new DurableObjectRpcError({
497
+ binding: definition.binding,
498
+ method: methodName,
499
+ cause,
500
+ }),
501
+ ),
502
+ );
504
503
 
505
- const scopedCall = <Method extends StubMethodKey<Api>>(
506
- stub: StubClient,
507
- method: Method,
508
- ...args: StubMethodArgs<Api, Method>
509
- ) =>
510
- Effect.gen(function* () {
511
- const methodName = String(method);
512
- const result = yield* rpc(stub, method, ...args);
513
- const value = yield* CloudflareRpc.scoped(result);
514
-
515
- return yield* decodeSuccess<Method>(methodName, value);
516
- });
504
+ return yield* decodeSuccess<Method>(methodName, value);
505
+ });
506
+
507
+ const scopedCall = Effect.fn("DurableObjectNamespace.scopedCall")(function* <
508
+ Method extends StubMethodKey<Api>,
509
+ >(stub: StubClient, method: Method, ...args: StubMethodArgs<Api, Method>) {
510
+ const methodName = String(method);
511
+ const result = yield* rpc(stub, method, ...args);
512
+ const value = yield* CloudflareRpc.scoped(result);
513
+
514
+ return yield* decodeSuccess<Method>(methodName, value);
515
+ });
517
516
 
518
517
  const directMethods = makeDirectMethods<never, Api, Definition>(definition.definition, {
519
518
  call,
@@ -533,7 +532,7 @@ export const makeClient = <
533
532
  rpc,
534
533
  call,
535
534
  scopedCall,
536
- unsafeRaw: Effect.succeed(namespace),
535
+ rawUnsafe: Effect.succeed(namespace),
537
536
  }) as DurableObjectNamespaceEffectClient<Api, Definition>;
538
537
  };
539
538
  };
@@ -545,7 +544,11 @@ export const layer = <
545
544
  >(
546
545
  tag: Context.Service<Self, DurableObjectNamespaceEffectClient<Api, Definition>>,
547
546
  definition: DurableObjectNamespaceBindingDefinition<Definition>,
548
- ) =>
547
+ ): Layer.Layer<
548
+ Self,
549
+ Binding.BindingNotFoundError | Binding.BindingValidationError,
550
+ WorkerEnvironment
551
+ > =>
549
552
  Binding.layer(
550
553
  tag,
551
554
  definition.binding,
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Layer, Option, Queue } from "effect";
1
+ import { Context, Effect, Layer, Predicate, Queue } from "effect";
2
2
  import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
3
3
  import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
4
4
  import * as RpcServer from "effect/unstable/rpc/RpcServer";
@@ -52,7 +52,13 @@ export class DurableObjectRpcWebSocket extends Context.Service<
52
52
  /**
53
53
  * Builds a layer that bridges Durable Object websocket events to `RpcServer.Protocol`.
54
54
  */
55
- export const layer = (options: LayerOptions = {}) =>
55
+ export const layer = (
56
+ options: LayerOptions = {},
57
+ ): Layer.Layer<
58
+ RpcServer.Protocol | DurableObjectRpcWebSocket,
59
+ never,
60
+ DurableObjectState | RpcSerialization.RpcSerialization
61
+ > =>
56
62
  Layer.effectContext(
57
63
  Effect.gen(function* () {
58
64
  const tag = options.tag ?? defaultTag;
@@ -156,7 +162,7 @@ export const layer = (options: LayerOptions = {}) =>
156
162
  connection?.socket.raw.close();
157
163
  }),
158
164
  clientIds: Effect.sync(() => clientIds),
159
- initialMessage: Effect.succeed(Option.none()),
165
+ initialMessage: Effect.succeedNone,
160
166
  supportsAck: true,
161
167
  supportsTransferables: false,
162
168
  supportsSpanPropagation: true,
@@ -174,7 +180,7 @@ export const layer = (options: LayerOptions = {}) =>
174
180
  const connection = register(socket);
175
181
  const decoded = yield* Effect.try({
176
182
  try: () => connection.parser.decode(normalizeMessage(message)),
177
- catch: RpcMessage.ResponseDefectEncoded,
183
+ catch: (cause) => cause,
178
184
  });
179
185
 
180
186
  const run = writeRequest;
@@ -189,10 +195,12 @@ export const layer = (options: LayerOptions = {}) =>
189
195
  yield* run(connection.id, current as RpcMessage.FromClientEncoded);
190
196
  }
191
197
  }).pipe(
192
- Effect.catch((error) => {
198
+ Effect.catch((cause) => {
193
199
  const connection = register(socket);
194
200
 
195
- return send(connection, error);
201
+ return Predicate.isTagged(cause, "MaxBufferSizeExceeded")
202
+ ? Effect.ignore(connection.socket.close(1009, String(cause)))
203
+ : send(connection, RpcMessage.ResponseDefectEncoded(cause));
196
204
  }),
197
205
  ),
198
206
  close: unregister,
@@ -1,10 +1,11 @@
1
1
  import { SqliteClient } from "@effect/sql-sqlite-do";
2
2
  import { Effect, Layer } from "effect";
3
+ import type { SqlClient } from "effect/unstable/sql";
3
4
 
4
5
  import { DurableObjectState } from "./DurableObjectState";
5
6
 
6
7
  /** Options forwarded to `@effect/sql-sqlite-do` when building a SQL client layer. */
7
- export type SqliteLayerOptions = Omit<SqliteClient.SqliteClientConfig, "db">;
8
+ export type SqliteLayerOptions = Omit<SqliteClient.SqliteClientConfig, "db" | "storage">;
8
9
 
9
10
  /**
10
11
  * Provides `effect/unstable/sql` from the current Durable Object SQLite storage.
@@ -17,11 +18,13 @@ export type SqliteLayerOptions = Omit<SqliteClient.SqliteClientConfig, "db">;
17
18
  * );
18
19
  * ```
19
20
  */
20
- export const layer = (options?: SqliteLayerOptions) =>
21
+ export const layer = (
22
+ options?: SqliteLayerOptions,
23
+ ): Layer.Layer<SqliteClient.SqliteClient | SqlClient.SqlClient, never, DurableObjectState> =>
21
24
  Layer.unwrap(
22
25
  Effect.gen(function* () {
23
26
  const state = yield* DurableObjectState;
24
27
 
25
- return SqliteClient.layer({ ...options, db: state.raw.storage.sql });
28
+ return SqliteClient.layer({ ...options, storage: state.raw.storage });
26
29
  }),
27
30
  );
@@ -81,9 +81,7 @@ export const fromDurableObjectState = (
81
81
  Effect.context<R>().pipe(
82
82
  Effect.flatMap((context) =>
83
83
  Effect.promise(() =>
84
- state.blockConcurrencyWhile(() =>
85
- runPromiseExitPreservingTypedFailures(Effect.provideContext(effect, context)),
86
- ),
84
+ state.blockConcurrencyWhile(() => runPromiseExitPreservingTypedFailures(context, effect)),
87
85
  ),
88
86
  ),
89
87
  Effect.flatten,
@@ -92,9 +90,7 @@ export const fromDurableObjectState = (
92
90
  Effect.context<R>().pipe(
93
91
  Effect.flatMap((context) =>
94
92
  Effect.promise(() =>
95
- state.blockConcurrencyWhile(() =>
96
- Effect.runPromise(Effect.provideContext(effect, context)),
97
- ),
93
+ state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect)),
98
94
  ),
99
95
  ),
100
96
  ),
@@ -116,10 +112,11 @@ export const fromDurableObjectState = (
116
112
  abort: (reason?: string) => Effect.sync(() => state.abort(reason)),
117
113
  });
118
114
 
119
- const runPromiseExitPreservingTypedFailures = async <A, E>(
120
- effect: Effect.Effect<A, E>,
115
+ const runPromiseExitPreservingTypedFailures = async <A, E, R>(
116
+ context: Context.Context<R>,
117
+ effect: Effect.Effect<A, E, R>,
121
118
  ): Promise<Exit.Exit<A, E>> => {
122
- const exit = await Effect.runPromiseExit(effect);
119
+ const exit = await Effect.runPromiseExitWith(context)(effect);
123
120
 
124
121
  if (Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause))) {
125
122
  throw Cause.squash(exit.cause);
@@ -1,5 +1,7 @@
1
1
  import { Data, Effect, Exit, Option, Schema as S } from "effect";
2
2
 
3
+ import * as ErrorMessage from "./internal/ErrorMessage";
4
+
3
5
  /** Supported primitive value types for Durable Object SQL APIs. */
4
6
  export type SqlStorageValue = globalThis.SqlStorageValue;
5
7
 
@@ -7,7 +9,11 @@ export type SqlStorageValue = globalThis.SqlStorageValue;
7
9
  export class StorageOperationError extends Data.TaggedError("StorageOperationError")<{
8
10
  readonly operation: string;
9
11
  readonly cause: unknown;
10
- }> {}
12
+ }> {
13
+ override get message(): string {
14
+ return `Durable Object storage ${this.operation} failed: ${ErrorMessage.causeMessage(this.cause)}`;
15
+ }
16
+ }
11
17
 
12
18
  type StorageEffect<A> = Effect.Effect<A, StorageOperationError>;
13
19
 
@@ -1,6 +1,7 @@
1
- import { Data, Effect, Option, Schema as S } from "effect";
1
+ import { Data, Effect, Option, Result, Schema as S } from "effect";
2
2
 
3
3
  import { DurableObjectState } from "./DurableObjectState";
4
+ import * as ErrorMessage from "./internal/ErrorMessage";
4
5
 
5
6
  /** Data supported by Cloudflare websocket `send`. */
6
7
  export type DurableWebSocketSendData = string | ArrayBuffer | ArrayBufferView;
@@ -8,12 +9,20 @@ export type DurableWebSocketSendData = string | ArrayBuffer | ArrayBufferView;
8
9
  /** Error raised when sending on a Durable Object websocket fails. */
9
10
  export class DurableWebSocketSendError extends Data.TaggedError("DurableWebSocketSendError")<{
10
11
  readonly cause: unknown;
11
- }> {}
12
+ }> {
13
+ override get message(): string {
14
+ return `Durable Object websocket send failed: ${ErrorMessage.causeMessage(this.cause)}`;
15
+ }
16
+ }
12
17
 
13
18
  /** Error raised when closing a Durable Object websocket fails. */
14
19
  export class DurableWebSocketCloseError extends Data.TaggedError("DurableWebSocketCloseError")<{
15
20
  readonly cause: unknown;
16
- }> {}
21
+ }> {
22
+ override get message(): string {
23
+ return `Durable Object websocket close failed: ${ErrorMessage.causeMessage(this.cause)}`;
24
+ }
25
+ }
17
26
 
18
27
  /** Error raised when serializing or deserializing a websocket attachment fails. */
19
28
  export class DurableWebSocketAttachmentError extends Data.TaggedError(
@@ -21,7 +30,11 @@ export class DurableWebSocketAttachmentError extends Data.TaggedError(
21
30
  )<{
22
31
  readonly operation: "serialize" | "deserialize";
23
32
  readonly cause: unknown;
24
- }> {}
33
+ }> {
34
+ override get message(): string {
35
+ return `Durable Object websocket attachment ${this.operation} failed: ${ErrorMessage.causeMessage(this.cause)}`;
36
+ }
37
+ }
25
38
 
26
39
  /** Effect-native wrapper around a hibernatable Durable Object websocket. */
27
40
  export interface DurableWebSocket<Attachment = unknown> {
@@ -110,33 +123,35 @@ export interface AcceptedUpgrade<Attachment = unknown> {
110
123
  * return response.response;
111
124
  * ```
112
125
  */
113
- export const acceptUpgrade = <Attachment = unknown>(
126
+ export const acceptUpgrade = Effect.fn("DurableObjectWebSocket.acceptUpgrade")(function* <
127
+ Attachment = unknown,
128
+ >(
114
129
  options: AcceptUpgradeOptions<Attachment> = {},
115
- ): Effect.Effect<AcceptedUpgrade<Attachment>, never, DurableObjectState> =>
116
- Effect.gen(function* () {
117
- const state = yield* DurableObjectState;
118
- const pair = new WebSocketPair();
119
- const client = pair[0];
120
- const server = fromWebSocket<Attachment>(pair[1]);
121
-
122
- if (options.attachment !== undefined) {
123
- server.raw.serializeAttachment(options.attachment);
124
- }
125
-
126
- yield* state.acceptWebSocket(
127
- server,
128
- options.tags === undefined ? undefined : [...options.tags],
129
- );
130
+ ): Effect.fn.Return<
131
+ AcceptedUpgrade<Attachment>,
132
+ DurableWebSocketAttachmentError,
133
+ DurableObjectState
134
+ > {
135
+ const state = yield* DurableObjectState;
136
+ const pair = new WebSocketPair();
137
+ const client = pair[0];
138
+ const server = fromWebSocket<Attachment>(pair[1]);
139
+
140
+ if (options.attachment !== undefined) {
141
+ yield* server.serializeAttachment(options.attachment);
142
+ }
130
143
 
131
- return {
132
- client,
133
- server,
134
- response: new Response(null, {
135
- status: 101,
136
- webSocket: client,
137
- }),
138
- };
139
- });
144
+ yield* state.acceptWebSocket(server, options.tags === undefined ? undefined : [...options.tags]);
145
+
146
+ return {
147
+ client,
148
+ server,
149
+ response: new Response(null, {
150
+ status: 101,
151
+ webSocket: client,
152
+ }),
153
+ };
154
+ });
140
155
 
141
156
  export type AttachmentInvalidPolicy = "ignore" | "ignore-and-close" | "fail";
142
157
 
@@ -213,23 +228,18 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
213
228
  const onInvalid = options.onInvalid ?? "ignore";
214
229
 
215
230
  for (const socket of sockets) {
216
- const decoded = yield* deserialize(socket).pipe(
217
- Effect.match({
218
- onFailure: (error) => ({ _tag: "Failure" as const, error }),
219
- onSuccess: (value) => ({ _tag: "Success" as const, value }),
220
- }),
221
- );
222
-
223
- if (decoded._tag === "Success" && Option.isSome(decoded.value)) {
224
- restored.push({ socket, attachment: decoded.value.value });
231
+ const decoded = yield* Effect.result(deserialize(socket));
232
+
233
+ if (Result.isSuccess(decoded) && Option.isSome(decoded.success)) {
234
+ restored.push({ socket, attachment: decoded.success.value });
225
235
  continue;
226
236
  }
227
237
 
228
- if (decoded._tag === "Failure" && onInvalid === "fail") {
229
- return yield* Effect.fail(decoded.error);
238
+ if (Result.isFailure(decoded) && onInvalid === "fail") {
239
+ return yield* Effect.fail(decoded.failure);
230
240
  }
231
241
 
232
- if (decoded._tag === "Failure" && onInvalid === "ignore-and-close") {
242
+ if (Result.isFailure(decoded) && onInvalid === "ignore-and-close") {
233
243
  yield* socket.close(1008, "invalid websocket attachment").pipe(Effect.ignore);
234
244
  }
235
245
  }
package/src/Email.ts CHANGED
@@ -9,6 +9,7 @@ import { Context, Data, Effect, type Layer } from "effect";
9
9
 
10
10
  import * as Binding from "./Binding";
11
11
  import type { WorkerEnvironment } from "./Environment";
12
+ import * as ErrorMessage from "./internal/ErrorMessage";
12
13
 
13
14
  const expectedSendEmailBinding = "Send Email binding with send()";
14
15
  const textEncoder = new TextEncoder();
@@ -27,7 +28,14 @@ export const sendLimits = {
27
28
  maxAllowlistHeaders: 20,
28
29
  } as const;
29
30
 
30
- /** Error codes reported by Cloudflare Email Sending. */
31
+ /**
32
+ * Error codes reported by Cloudflare Email Sending.
33
+ *
34
+ * Surfaced on {@link EmailOperationError} `code` on a best-effort basis: the
35
+ * runtime attaches them as a non-standard `code` property on thrown errors,
36
+ * which may be missing depending on the runtime or dev-time proxying
37
+ * (miniflare remote bindings serialize errors and can strip it).
38
+ */
31
39
  export const emailErrorCodes = [
32
40
  "E_VALIDATION_ERROR",
33
41
  "E_FIELD_MISSING",
@@ -59,9 +67,21 @@ export class EmailOperationError extends Data.TaggedError("EmailOperationError")
59
67
  readonly binding: string;
60
68
  readonly operation: string;
61
69
  readonly cause: unknown;
62
- /** Cloudflare error code taken from the thrown error, when present. */
70
+ /**
71
+ * Cloudflare error code taken from the thrown error, when present.
72
+ *
73
+ * Best-effort: may be `undefined` depending on the runtime or dev-time
74
+ * proxying — miniflare remote bindings (`"remote": true`) serialize thrown
75
+ * errors and can strip the non-standard `code` property.
76
+ */
63
77
  readonly code?: EmailErrorCode | (string & {});
64
- }> {}
78
+ }> {
79
+ override get message(): string {
80
+ const code = this.code === undefined ? "" : ` (${this.code})`;
81
+
82
+ return `Email ${this.operation} failed for binding "${this.binding}"${code}: ${ErrorMessage.causeMessage(this.cause)}`;
83
+ }
84
+ }
65
85
 
66
86
  /** A single message field that violates a documented Email Sending limit. */
67
87
  export interface EmailViolation {
@@ -76,7 +96,11 @@ export class EmailValidationError extends Data.TaggedError("EmailValidationError
76
96
  readonly binding: string;
77
97
  readonly operation: string;
78
98
  readonly violations: ReadonlyArray<EmailViolation>;
79
- }> {}
99
+ }> {
100
+ override get message(): string {
101
+ return `Email ${this.operation} for binding "${this.binding}" failed validation: ${ErrorMessage.violationsMessage(this.violations)}`;
102
+ }
103
+ }
80
104
 
81
105
  /** Typed Cloudflare Send Email binding definition. */
82
106
  export interface EmailDefinition {
@@ -108,7 +132,7 @@ export interface EmailClient {
108
132
  (message: EmailMessage): Effect.Effect<EmailSendResult, EmailSendError>;
109
133
  (builder: EmailMessageBuilder): Effect.Effect<EmailSendResult, EmailSendError>;
110
134
  };
111
- readonly unsafeRaw: Effect.Effect<EmailBinding>;
135
+ readonly rawUnsafe: Effect.Effect<EmailBinding>;
112
136
  readonly definition: EmailDefinition;
113
137
  }
114
138
 
@@ -141,6 +165,12 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
141
165
  >;
142
166
  }
143
167
 
168
+ /**
169
+ * Best-effort extraction of the Cloudflare error `code` from a thrown error.
170
+ * Returns `undefined` when the property is absent — e.g. when a dev-time
171
+ * proxy such as a miniflare remote binding (`"remote": true`) serializes the
172
+ * error and strips non-standard properties.
173
+ */
144
174
  const emailErrorCode = (cause: unknown): EmailOperationError["code"] => {
145
175
  if (typeof cause !== "object" || cause === null) {
146
176
  return undefined;
@@ -439,21 +469,18 @@ export const makeClient =
439
469
  (email: EmailBinding): EmailClient => {
440
470
  const runtime = email as EmailRuntimeBinding;
441
471
  const validate = options?.validate ?? true;
442
- const send = ((message: EmailSendInput) => {
443
- const dispatch = () =>
444
- tryEmailPromise(definition.binding, "send", () => runtime.send(message));
445
-
446
- if (!validate || !isEmailMessageBuilder(message)) {
447
- return dispatch();
472
+ const send = Effect.fn("Email.send")(function* (message: EmailSendInput) {
473
+ if (validate && isEmailMessageBuilder(message)) {
474
+ yield* validateSendInput(definition.binding, "send", message);
448
475
  }
449
476
 
450
- return validateSendInput(definition.binding, "send", message).pipe(Effect.flatMap(dispatch));
477
+ return yield* tryEmailPromise(definition.binding, "send", () => runtime.send(message));
451
478
  }) as EmailClient["send"];
452
479
 
453
480
  return {
454
481
  definition,
455
482
  send,
456
- unsafeRaw: Effect.succeed(email),
483
+ rawUnsafe: Effect.succeed(email),
457
484
  };
458
485
  };
459
486