effect-cf 0.27.0 → 0.28.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 (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/{Artifacts-DcoXcRP5.d.mts → Artifacts-DnHQA-Ax.d.mts} +42 -25
  3. package/dist/{Artifacts-BhWrdyqX.mjs → Artifacts-rq3hUKQE.mjs} +46 -30
  4. package/dist/{Binding-COCtoJXa.d.mts → Binding-xJJJPmiD.d.mts} +6 -5
  5. package/dist/ComputerArtifacts.d.mts +1 -1
  6. package/dist/ComputerArtifacts.mjs +9 -9
  7. package/dist/ComputerWorkspace.d.mts +5 -5
  8. package/dist/ComputerWorkspace.mjs +107 -75
  9. package/dist/ComputerWorkspaceHost.d.mts +1 -1
  10. package/dist/ComputerWorkspaceHost.mjs +18 -16
  11. package/dist/{DurableObjectState-biHrHwrh.d.mts → DurableObjectState-B3uFY1eM.d.mts} +85 -62
  12. package/dist/{DurableObjectState-Dux8KfjG.mjs → DurableObjectState-BjkK2kwH.mjs} +3 -3
  13. package/dist/{ErrorMessage-_lg53p3W.mjs → ErrorMessage-BM0K-Nik.mjs} +1 -4
  14. package/dist/{Hyperdrive-6p59yyVk.d.mts → Hyperdrive-z-9fFz15.d.mts} +2 -2
  15. package/dist/HyperdrivePg.d.mts +2 -2
  16. package/dist/Vitest.d.mts +6 -5
  17. package/dist/Vitest.mjs +2 -2
  18. package/dist/{WorkflowBinding-zlRPCXVf.d.mts → WorkflowBinding-wZ-yGHow.d.mts} +5 -10
  19. package/dist/index.d.mts +82 -60
  20. package/dist/index.mjs +268 -170
  21. package/package.json +1 -1
  22. package/src/AiGateway.ts +62 -7
  23. package/src/AnalyticsEngine.ts +17 -13
  24. package/src/Artifacts.ts +111 -47
  25. package/src/Binding.ts +34 -23
  26. package/src/BrowserRendering.ts +72 -33
  27. package/src/CloudflareOtlp.ts +8 -5
  28. package/src/ComputerArtifacts.ts +17 -9
  29. package/src/ComputerWorkspace.ts +202 -98
  30. package/src/ComputerWorkspaceHost.ts +55 -27
  31. package/src/ContainerNamespace.ts +8 -9
  32. package/src/D1.ts +8 -14
  33. package/src/DurableObject.ts +27 -11
  34. package/src/DurableObjectAlarm.ts +17 -5
  35. package/src/DurableObjectDefinition.ts +89 -72
  36. package/src/DurableObjectNamespace.ts +26 -12
  37. package/src/DurableObjectRpcWebSocket.ts +9 -10
  38. package/src/DurableObjectState.ts +1 -0
  39. package/src/DurableObjectStorage.ts +5 -2
  40. package/src/DurableObjectWebSocket.ts +4 -1
  41. package/src/Email.ts +26 -25
  42. package/src/Hyperdrive.ts +4 -10
  43. package/src/Images.ts +12 -14
  44. package/src/Kv.ts +15 -17
  45. package/src/Queue.ts +2 -1
  46. package/src/QueueBinding.ts +7 -12
  47. package/src/QueueDefinition.ts +7 -2
  48. package/src/R2.ts +33 -24
  49. package/src/Rpc.ts +15 -21
  50. package/src/RpcDefinition.ts +33 -38
  51. package/src/ServiceBinding.ts +26 -8
  52. package/src/Vectorize.ts +45 -18
  53. package/src/Vitest.ts +9 -3
  54. package/src/WebTransport.ts +11 -5
  55. package/src/Worker.ts +91 -21
  56. package/src/WorkerDefinition.ts +143 -63
  57. package/src/WorkersAi.ts +50 -28
  58. package/src/Workflow.ts +66 -13
  59. package/src/WorkflowBinding.ts +18 -25
  60. package/src/WorkflowDefinition.ts +4 -2
  61. package/src/internal/Entrypoint.ts +29 -13
  62. package/src/internal/ErrorMessage.ts +7 -6
  63. package/src/internal/RpcInvocation.ts +34 -11
  64. package/src/internal/Runtime.ts +33 -22
  65. package/src/internal/WorkerContext.ts +3 -3
@@ -1,4 +1,4 @@
1
- import { Data, Effect, type Context, type Layer, type Scope } from "effect";
1
+ import { Data, Effect, Predicate, type Context, type Layer, type Scope } from "effect";
2
2
 
3
3
  import * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
@@ -17,13 +17,18 @@ interface DurableObjectFetcher {
17
17
 
18
18
  type RpcClient<Api> = {
19
19
  readonly [Key in keyof Api as Key extends string
20
- ? Api[Key] extends (...args: Array<any>) => unknown
20
+ ? Api[Key] extends (...args: Array<any>) => any
21
21
  ? Key
22
22
  : never
23
23
  : never]: Api[Key];
24
24
  };
25
25
 
26
26
  type ReservedMethodName = DurableObjectDefinition.ReservedMethodName | "fetch";
27
+ type DynamicMethod = (...args: Array<any>) => any;
28
+ interface DynamicMethods {
29
+ [method: string]: DynamicMethod;
30
+ }
31
+ type NamespaceCandidate = Parameters<typeof Predicate.isUnknown>[0];
27
32
 
28
33
  /**
29
34
  * Cloudflare Durable Object stub, optionally enriched with RPC methods.
@@ -373,14 +378,12 @@ export type DurableObjectNamespaceStaticClient<
373
378
  readonly rawUnsafe: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
374
379
  };
375
380
 
376
- const hasFunction = (value: object, key: string): boolean =>
377
- typeof Reflect.get(value, key) === "function";
381
+ const hasFunction = <Candidate>(value: Candidate, key: string): boolean =>
382
+ Predicate.hasProperty(value, key) && Predicate.isFunction(value[key]);
378
383
 
379
384
  export const isDurableObjectNamespaceClient = <Api extends object>(
380
- value: unknown,
385
+ value: NamespaceCandidate,
381
386
  ): value is DurableObjectNamespaceClient<Api> =>
382
- typeof value === "object" &&
383
- value !== null &&
384
387
  hasFunction(value, "getByName") &&
385
388
  hasFunction(value, "get") &&
386
389
  hasFunction(value, "idFromName") &&
@@ -430,6 +433,7 @@ export const makeClient = <
430
433
  Method extends StubMethodKey<Api>,
431
434
  >(stub: StubClient, method: Method, ...args: StubMethodArgs<Api, Method>) {
432
435
  const methodName = String(method);
436
+ // SAFETY: the runtime definition and method name select the same method schemas as Method.
433
437
  const encodedArgs =
434
438
  definition.definition === undefined
435
439
  ? args
@@ -448,6 +452,7 @@ export const makeClient = <
448
452
  ),
449
453
  );
450
454
 
455
+ // SAFETY: encodedArgs preserves the selected method's positional argument contract.
451
456
  return yield* RpcInvocation.invokeRpcMethod(
452
457
  stub,
453
458
  method,
@@ -465,9 +470,11 @@ export const makeClient = <
465
470
  Method extends StubMethodKey<Api>,
466
471
  >(methodName: string, value: Awaited<StubMethodCloudflareReturn<Api, Method>>) {
467
472
  if (definition.definition === undefined) {
473
+ // SAFETY: without a schema codec, the native RPC result is already the declared success type.
468
474
  return value as StubMethodSuccess<Api, Method>;
469
475
  }
470
476
 
477
+ // SAFETY: methodName identifies Method in the same definition used to decode the result.
471
478
  const decoded = yield* RpcDefinition.decodeSuccess(
472
479
  definition.definition,
473
480
  methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
@@ -483,6 +490,7 @@ export const makeClient = <
483
490
  ),
484
491
  );
485
492
 
493
+ // SAFETY: the selected method's success schema decodes to its declared API success type.
486
494
  return decoded as StubMethodSuccess<Api, Method>;
487
495
  });
488
496
 
@@ -521,6 +529,7 @@ export const makeClient = <
521
529
  getByName,
522
530
  });
523
531
 
532
+ // SAFETY: the common namespace operations and generated direct methods implement the client contract.
524
533
  return Object.assign(directMethods, {
525
534
  newUniqueId,
526
535
  idFromName,
@@ -581,47 +590,51 @@ export const makeDirectMethods = <
581
590
  ) => Effect.Effect<DurableObjectStubClient<Api>, never, R>;
582
591
  },
583
592
  ): DirectMethods<R, Api, Definition> => {
584
- const methods = {} as Record<string, unknown>;
593
+ const methods: DynamicMethods = {};
585
594
 
586
595
  if (rpcDefinition !== undefined) {
587
596
  for (const methodName of Object.keys(rpcDefinition.methods)) {
597
+ // SAFETY: methodName comes from rpcDefinition.methods and helpers.call dispatches that same key.
588
598
  methods[methodName] = (stub: DurableObjectStubClient<Api>, ...args: Array<unknown>) =>
589
599
  (
590
600
  helpers.call as (
591
601
  stub: DurableObjectStubClient<Api>,
592
602
  method: string,
593
603
  ...args: Array<unknown>
594
- ) => unknown
604
+ ) => any
595
605
  )(stub, methodName, ...args);
596
606
  }
597
607
 
598
608
  const makeClient = (
599
609
  getStub: () => Effect.Effect<DurableObjectStubClient<Api>, never, R>,
600
610
  ): DurableObjectDirectClient<R, Api> => {
601
- const client = {
611
+ const client: DynamicMethods = {
602
612
  fetch: (input: RequestInfo | URL, init?: RequestInit) =>
603
613
  Effect.gen(function* () {
604
614
  const stub = yield* getStub();
605
615
 
606
616
  return yield* helpers.fetch(stub, input, init);
607
617
  }),
608
- } as Record<string, unknown>;
618
+ };
609
619
 
610
620
  for (const methodName of Object.keys(rpcDefinition.methods)) {
621
+ // SAFETY: methodName comes from rpcDefinition.methods and helpers.call dispatches that same key.
611
622
  client[methodName] = (...args: Array<unknown>) =>
612
623
  Effect.gen(function* () {
613
624
  const stub = yield* getStub();
614
625
 
626
+ // SAFETY: methodName and arguments come from the same generated RPC method entry.
615
627
  return yield* (
616
628
  helpers.call as (
617
629
  stub: DurableObjectStubClient<Api>,
618
630
  method: string,
619
631
  ...args: Array<unknown>
620
- ) => Effect.Effect<unknown, DurableObjectRpcError, R>
632
+ ) => Effect.Effect<any, DurableObjectRpcError, R>
621
633
  )(stub, methodName, ...args);
622
634
  });
623
635
  }
624
636
 
637
+ // SAFETY: client contains fetch plus every RPC method enumerated by rpcDefinition.
625
638
  return client as DurableObjectDirectClient<R, Api>;
626
639
  };
627
640
 
@@ -636,5 +649,6 @@ export const makeDirectMethods = <
636
649
  ) => makeClient(() => helpers.get(id, options));
637
650
  }
638
651
 
652
+ // SAFETY: methods is populated from the exact rpcDefinition keys, including its by-name/id clients.
639
653
  return methods as DirectMethods<R, Api, Definition>;
640
654
  };
@@ -32,7 +32,7 @@ export interface DurableObjectRpcWebSocketService {
32
32
  message: NativeWebSocketMessage,
33
33
  ) => Effect.Effect<void>;
34
34
  readonly close: (socket: DurableWebSocket) => Effect.Effect<void>;
35
- readonly error: (socket: DurableWebSocket, error: unknown) => Effect.Effect<void>;
35
+ readonly error: (socket: DurableWebSocket, cause: unknown) => Effect.Effect<void>;
36
36
  }
37
37
 
38
38
  interface RpcConnection {
@@ -192,6 +192,7 @@ export const layer = (
192
192
  }
193
193
 
194
194
  for (const current of decoded) {
195
+ // SAFETY: this parser was created by the configured RPC serialization service.
195
196
  yield* run(connection.id, current as RpcMessage.FromClientEncoded);
196
197
  }
197
198
  }).pipe(
@@ -204,9 +205,9 @@ export const layer = (
204
205
  }),
205
206
  ),
206
207
  close: unregister,
207
- error: (socket, error) =>
208
+ error: (socket, cause) =>
208
209
  Effect.gen(function* () {
209
- yield* Effect.logDebug("Durable Object RPC websocket error", error);
210
+ yield* Effect.logDebug("Durable Object RPC websocket error", cause);
210
211
  yield* unregister(socket);
211
212
  }),
212
213
  });
@@ -219,16 +220,15 @@ export const layer = (
219
220
  );
220
221
 
221
222
  const normalizeMessage = (message: NativeWebSocketMessage) =>
222
- typeof message === "string" ? message : new Uint8Array(message);
223
+ Predicate.isString(message) ? message : new Uint8Array(message);
223
224
 
224
225
  const readAttachment = (socket: WebSocket, key: string): number | undefined => {
225
226
  const value = socket.deserializeAttachment();
226
227
 
227
228
  if (
228
- value !== null &&
229
- typeof value === "object" &&
230
- key in value &&
231
- typeof value[key] === "number"
229
+ Predicate.isObjectOrArray(value) &&
230
+ Predicate.hasProperty(value, key) &&
231
+ Predicate.isNumber(value[key])
232
232
  ) {
233
233
  return value[key];
234
234
  }
@@ -238,8 +238,7 @@ const readAttachment = (socket: WebSocket, key: string): number | undefined => {
238
238
 
239
239
  const writeAttachment = (socket: WebSocket, key: string, clientId: number) => {
240
240
  const current = socket.deserializeAttachment();
241
- const attachment =
242
- current !== null && typeof current === "object" && !Array.isArray(current) ? current : {};
241
+ const attachment = Predicate.isObject(current) ? current : {};
243
242
 
244
243
  socket.serializeAttachment({
245
244
  ...attachment,
@@ -88,6 +88,7 @@ export const fromDurableObjectState = (
88
88
  const scheduleWaitUntil = makeWaitUntilScheduler("DurableObjectState.waitUntil", (promise) =>
89
89
  state.waitUntil(promise),
90
90
  );
91
+ // SAFETY: the runtime Effect.isEffect branch implements both overloads of the service method.
91
92
  const waitUntil = (<A, E, R, R2 = never>(
92
93
  input: Promise<unknown> | Effect.Effect<A, E, R>,
93
94
  options?: WorkerContextWaitUntilOptions<E, R2>,
@@ -1,4 +1,4 @@
1
- import { Data, Effect, Exit, Option, Schema as S } from "effect";
1
+ import { Data, Effect, Exit, Option, Predicate, Schema as S } from "effect";
2
2
 
3
3
  import * as ErrorMessage from "./internal/ErrorMessage";
4
4
 
@@ -260,6 +260,7 @@ const fromSyncKvStorage = (kv: globalThis.SyncKvStorage): SyncKvStorage => ({
260
260
  schemaBackedSyncKvStorage(kv, definition),
261
261
  });
262
262
 
263
+ // SAFETY: every overload is dispatched by the native key form and wrapped with the same Effect error contract.
263
264
  const fromDurableObjectTransaction = (
264
265
  txn: globalThis.DurableObjectTransaction,
265
266
  ): DurableObjectTransaction =>
@@ -279,7 +280,7 @@ const fromDurableObjectTransaction = (
279
280
  maybeOptions?: globalThis.DurableObjectPutOptions,
280
281
  ) =>
281
282
  tryStoragePromise("transaction.put", () =>
282
- typeof keyOrEntries === "string"
283
+ Predicate.isString(keyOrEntries)
283
284
  ? txn.put(keyOrEntries, valueOrOptions as T, maybeOptions)
284
285
  : txn.put(keyOrEntries, valueOrOptions as globalThis.DurableObjectPutOptions | undefined),
285
286
  ),
@@ -334,6 +335,7 @@ export const fromDurableObjectStorage = (
334
335
  );
335
336
  } catch (cause) {
336
337
  if (Exit.isExit(cause) && Exit.isFailure(cause)) {
338
+ // SAFETY: Exit.isFailure proves this cause carries the closure's typed E failure.
337
339
  return Effect.failCause(cause.cause) as Effect.Effect<
338
340
  A,
339
341
  E | StorageOperationError,
@@ -366,6 +368,7 @@ export const fromDurableObjectStorage = (
366
368
  (value) => resume(Effect.succeed(value)),
367
369
  (cause) => {
368
370
  if (Exit.isExit(cause) && Exit.isFailure(cause)) {
371
+ // SAFETY: Exit.isFailure proves this cause carries the closure's typed E failure.
369
372
  resume(
370
373
  Effect.failCause(cause.cause) as Effect.Effect<A, E | StorageOperationError>,
371
374
  );
@@ -61,6 +61,7 @@ export const fromWebSocket = <Attachment = unknown>(
61
61
  const existing = wrappers.get(raw);
62
62
 
63
63
  if (existing !== undefined) {
64
+ // SAFETY: Attachment is phantom; a wrapper's runtime behavior is independent of that type.
64
65
  return existing as DurableWebSocket<Attachment>;
65
66
  }
66
67
 
@@ -89,6 +90,7 @@ export const fromWebSocket = <Attachment = unknown>(
89
90
 
90
91
  wrappers.set(raw, socket);
91
92
 
93
+ // SAFETY: Attachment is phantom; the newly created wrapper supports values through its generic serializer.
92
94
  return socket as DurableWebSocket<Attachment>;
93
95
  };
94
96
 
@@ -200,6 +202,7 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
200
202
  Effect.mapError(
201
203
  (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
202
204
  ),
205
+ // SAFETY: S.encodeEffect returns this codec's declared Encoded representation.
203
206
  Effect.flatMap((encoded) => socket.serializeAttachment(encoded as Encoded)),
204
207
  );
205
208
 
@@ -261,7 +264,7 @@ export interface DurableWebSocketHandlers<R = never, E = unknown> {
261
264
  reason: string,
262
265
  wasClean: boolean,
263
266
  ) => Effect.Effect<void, E, R>;
264
- readonly error?: (socket: DurableWebSocket, error: unknown) => Effect.Effect<void, E, R>;
267
+ readonly error?: (socket: DurableWebSocket, cause: unknown) => Effect.Effect<void, E, R>;
265
268
  }
266
269
 
267
270
  /** Maps compact websocket lifecycle handler names to `DurableObject.make` options. */
package/src/Email.ts CHANGED
@@ -4,7 +4,7 @@ import type {
4
4
  EmailSendResult as CloudflareEmailSendResult,
5
5
  SendEmail as CloudflareSendEmail,
6
6
  } from "@cloudflare/workers-types";
7
- import { Context, Data, Effect, type Layer } from "effect";
7
+ import { Context, Data, Effect, type Layer, Predicate } from "effect";
8
8
 
9
9
  import * as Binding from "./Binding";
10
10
  import type { WorkerEnvironment } from "./Environment";
@@ -114,6 +114,7 @@ export type EmailSendResult = CloudflareEmailSendResult;
114
114
  export type EmailBinding = CloudflareSendEmail;
115
115
  export type EmailRecipients = string | EmailAddress | ReadonlyArray<string | EmailAddress>;
116
116
  export type EmailSendError = EmailOperationError | EmailValidationError;
117
+ type EmailFieldCandidate = Parameters<typeof Predicate.isUnknown>[0];
117
118
 
118
119
  export interface EmailSendOptions {
119
120
  /** Validates builder messages against documented limits. Defaults to `true`. */
@@ -166,13 +167,13 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
166
167
  * error and strips non-standard properties.
167
168
  */
168
169
  const emailErrorCode = (cause: unknown): EmailOperationError["code"] => {
169
- if (typeof cause !== "object" || cause === null) {
170
+ if (!Predicate.isObject(cause)) {
170
171
  return undefined;
171
172
  }
172
173
 
173
- const code = Reflect.get(cause, "code");
174
+ const code = Predicate.hasProperty(cause, "code") ? cause.code : undefined;
174
175
 
175
- return typeof code === "string" ? code : undefined;
176
+ return Predicate.isString(code) ? code : undefined;
176
177
  };
177
178
 
178
179
  const emailError = (binding: string, operation: string, cause: unknown) =>
@@ -213,21 +214,25 @@ const addressList = (value: EmailRecipients | undefined): ReadonlyArray<string |
213
214
  return [];
214
215
  }
215
216
 
217
+ // SAFETY: the non-array branch is exactly the scalar member of EmailRecipients.
216
218
  return Array.isArray(value) ? value : [value as string | EmailAddress];
217
219
  };
218
220
 
219
- const addressViolations = (path: string, value: unknown): ReadonlyArray<EmailViolation> => {
220
- if (typeof value === "string") {
221
+ const addressViolations = (
222
+ path: string,
223
+ value: EmailFieldCandidate,
224
+ ): ReadonlyArray<EmailViolation> => {
225
+ if (Predicate.isString(value)) {
221
226
  return value.includes("@") ? [] : [{ path, message: "Email address must contain an @ symbol" }];
222
227
  }
223
228
 
224
- if (typeof value !== "object" || value === null) {
229
+ if (!Predicate.isObject(value)) {
225
230
  return [{ path, message: "Email address must be a string or { name, email } object" }];
226
231
  }
227
232
 
228
- const email = Reflect.get(value, "email");
233
+ const email = Predicate.hasProperty(value, "email") ? value.email : undefined;
229
234
 
230
- if (typeof email !== "string" || !email.includes("@")) {
235
+ if (!Predicate.isString(email) || !email.includes("@")) {
231
236
  return [{ path: `${path}.email`, message: "Email address must contain an @ symbol" }];
232
237
  }
233
238
 
@@ -255,16 +260,16 @@ const attachmentViolations = (
255
260
  const attachment = attachments[index];
256
261
  const path = `attachments[${index}]`;
257
262
 
258
- if (typeof attachment !== "object" || attachment === null) {
263
+ if (!Predicate.isObject(attachment)) {
259
264
  violations.push({ path, message: "Attachment must be an object" });
260
265
  continue;
261
266
  }
262
267
 
263
- if (typeof attachment.filename !== "string" || attachment.filename === "") {
268
+ if (!Predicate.isString(attachment.filename) || attachment.filename === "") {
264
269
  violations.push({ path: `${path}.filename`, message: "Attachment filename is required" });
265
270
  }
266
271
 
267
- if (typeof attachment.type !== "string" || attachment.type === "") {
272
+ if (!Predicate.isString(attachment.type) || attachment.type === "") {
268
273
  violations.push({ path: `${path}.type`, message: "Attachment MIME type is required" });
269
274
  }
270
275
 
@@ -272,7 +277,7 @@ const attachmentViolations = (
272
277
  violations.push({ path: `${path}.content`, message: "Attachment content is required" });
273
278
  }
274
279
 
275
- if (attachment.disposition === "inline" && typeof attachment.contentId !== "string") {
280
+ if (attachment.disposition === "inline" && !Predicate.isString(attachment.contentId)) {
276
281
  violations.push({
277
282
  path: `${path}.contentId`,
278
283
  message: "Inline attachments require a contentId",
@@ -299,7 +304,7 @@ const headerViolations = (headers: Record<string, string>): ReadonlyArray<EmailV
299
304
  );
300
305
  }
301
306
 
302
- if (typeof value !== "string") {
307
+ if (!Predicate.isString(value)) {
303
308
  violations.push({ path, message: "Header value must be a string" });
304
309
  continue;
305
310
  }
@@ -346,6 +351,7 @@ const headerViolations = (headers: Record<string, string>): ReadonlyArray<EmailV
346
351
  */
347
352
  export const validateMessage = (message: EmailMessageBuilder): ReadonlyArray<EmailViolation> => {
348
353
  const violations: Array<EmailViolation> = [];
354
+ // SAFETY: this wider view exists solely to validate runtime callers that bypass TypeScript.
349
355
  const fields = message as {
350
356
  readonly to?: EmailRecipients;
351
357
  readonly cc?: EmailRecipients;
@@ -369,11 +375,11 @@ export const validateMessage = (message: EmailMessageBuilder): ReadonlyArray<Ema
369
375
  violations.push(...addressViolations("replyTo", fields.replyTo));
370
376
  }
371
377
 
372
- if (typeof fields.subject !== "string" || fields.subject === "") {
378
+ if (!Predicate.isString(fields.subject) || fields.subject === "") {
373
379
  violations.push({ path: "subject", message: "A subject is required" });
374
380
  }
375
381
 
376
- if (typeof fields.text !== "string" && typeof fields.html !== "string") {
382
+ if (!Predicate.isString(fields.text) && !Predicate.isString(fields.html)) {
377
383
  violations.push({ path: "$", message: "A message requires either text or html content" });
378
384
  }
379
385
 
@@ -425,19 +431,13 @@ const validateSendInput = (
425
431
  : Effect.succeed(message);
426
432
  };
427
433
 
428
- export const isEmailBinding = (value: unknown): value is EmailBinding => {
429
- if (typeof value !== "object" || value === null) {
430
- return false;
431
- }
432
-
433
- const resource = value as Record<string, unknown>;
434
-
435
- return typeof resource.send === "function";
436
- };
434
+ export const isEmailBinding = <Candidate>(value: Candidate): value is Candidate & EmailBinding =>
435
+ Predicate.hasProperty(value, "send") && Predicate.isFunction(value.send);
437
436
 
438
437
  export const makeClient =
439
438
  (definition: EmailDefinition, options?: EmailSendOptions) =>
440
439
  (email: EmailBinding): EmailClient => {
440
+ // SAFETY: EmailBinding.send is the native promise API represented by EmailRuntimeBinding.
441
441
  const runtime = email as EmailRuntimeBinding;
442
442
  const validate = options?.validate ?? true;
443
443
  const send = Effect.fn("Email.send")(function* (message: EmailMessageBuilder) {
@@ -469,6 +469,7 @@ export const Tag =
469
469
 
470
470
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
471
471
 
472
+ // SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
472
473
  return Object.assign(tag, {
473
474
  id,
474
475
  layer: makeLayer,
package/src/Hyperdrive.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Effect, type Layer } from "effect";
1
+ import { Context, Effect, type Layer, Predicate } from "effect";
2
2
 
3
3
  import * as Binding from "./Binding";
4
4
  import type { WorkerEnvironment } from "./Environment";
@@ -45,15 +45,8 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
45
45
  >;
46
46
  }
47
47
 
48
- export const isHyperdrive = (value: unknown): value is Hyperdrive => {
49
- if (typeof value !== "object" || value === null) {
50
- return false;
51
- }
52
-
53
- const resource = value as Record<string, unknown>;
54
-
55
- return typeof resource.connectionString === "string";
56
- };
48
+ export const isHyperdrive = <Candidate>(value: Candidate): value is Candidate & Hyperdrive =>
49
+ Predicate.hasProperty(value, "connectionString") && Predicate.isString(value.connectionString);
57
50
 
58
51
  export const makeClient =
59
52
  (definition: HyperdriveDefinition) =>
@@ -80,6 +73,7 @@ export const Tag =
80
73
 
81
74
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
82
75
 
76
+ // SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
83
77
  return Object.assign(tag, {
84
78
  id,
85
79
  layer: makeLayer,
package/src/Images.ts CHANGED
@@ -16,7 +16,7 @@ import type {
16
16
  ImageUploadOptions as CloudflareImageUploadOptions,
17
17
  Response as CloudflareResponse,
18
18
  } from "@cloudflare/workers-types";
19
- import { Context, Data, Effect, Function, Option, type Layer } from "effect";
19
+ import { Context, Data, Effect, Function, Option, Predicate, type Layer } from "effect";
20
20
 
21
21
  import * as Binding from "./Binding";
22
22
  import type { WorkerEnvironment } from "./Environment";
@@ -224,21 +224,18 @@ const tryImagesSync = <A>(
224
224
  const maybe = <A>(value: A | null): Option.Option<A> =>
225
225
  value === null ? Option.none() : Option.some(value);
226
226
 
227
- const hasFunction = (value: object, key: string): boolean =>
228
- typeof Reflect.get(value, key) === "function";
227
+ const hasFunction = <Candidate>(value: Candidate, key: string): boolean =>
228
+ Predicate.hasProperty(value, key) && Predicate.isFunction(value[key]);
229
229
 
230
- const isHostedImagesBinding = (value: unknown): value is CloudflareHostedImagesBinding =>
231
- typeof value === "object" &&
232
- value !== null &&
233
- hasFunction(value, "image") &&
234
- hasFunction(value, "upload") &&
235
- hasFunction(value, "list");
230
+ const isHostedImagesBinding = <Candidate>(
231
+ value: Candidate,
232
+ ): value is Candidate & CloudflareHostedImagesBinding =>
233
+ hasFunction(value, "image") && hasFunction(value, "upload") && hasFunction(value, "list");
236
234
 
237
- export const isImagesBinding = (value: unknown): value is ImagesRuntimeBinding =>
238
- typeof value === "object" &&
239
- value !== null &&
240
- hasFunction(value, "info") &&
241
- hasFunction(value, "input");
235
+ export const isImagesBinding = <Candidate>(
236
+ value: Candidate,
237
+ ): value is Candidate & ImagesRuntimeBinding =>
238
+ hasFunction(value, "info") && hasFunction(value, "input");
242
239
 
243
240
  const wrapResult = (
244
241
  binding: string,
@@ -342,6 +339,7 @@ export const Tag =
342
339
 
343
340
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
344
341
 
342
+ // SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
345
343
  return Object.assign(tag, {
346
344
  id,
347
345
  layer: makeLayer,
package/src/Kv.ts CHANGED
@@ -2,7 +2,7 @@ import type {
2
2
  KVNamespaceListOptions as CloudflareKVNamespaceListOptions,
3
3
  KVNamespacePutOptions as CloudflareKVNamespacePutOptions,
4
4
  } from "@cloudflare/workers-types";
5
- import { Context, Data, Effect, Option, Schema as S, type Layer } from "effect";
5
+ import { Context, Data, Effect, Option, Predicate, Schema as S, type Layer } from "effect";
6
6
 
7
7
  import * as Binding from "./Binding";
8
8
  import type { WorkerEnvironment } from "./Environment";
@@ -144,21 +144,17 @@ const tryKvPromise = <A>(
144
144
  catch: (cause) => kvError(binding, operation, cause),
145
145
  });
146
146
 
147
- export const isKvNamespace = (value: unknown): value is KVNamespace => {
148
- if (typeof value !== "object" || value === null) {
149
- return false;
150
- }
151
-
152
- const resource = value as Record<string, unknown>;
153
-
154
- return (
155
- typeof resource.get === "function" &&
156
- typeof resource.put === "function" &&
157
- typeof resource.delete === "function" &&
158
- typeof resource.getWithMetadata === "function" &&
159
- typeof resource.list === "function"
160
- );
161
- };
147
+ export const isKvNamespace = <Candidate>(value: Candidate): value is Candidate & KVNamespace =>
148
+ Predicate.hasProperty(value, "get") &&
149
+ Predicate.isFunction(value.get) &&
150
+ Predicate.hasProperty(value, "put") &&
151
+ Predicate.isFunction(value.put) &&
152
+ Predicate.hasProperty(value, "delete") &&
153
+ Predicate.isFunction(value.delete) &&
154
+ Predicate.hasProperty(value, "getWithMetadata") &&
155
+ Predicate.isFunction(value.getWithMetadata) &&
156
+ Predicate.hasProperty(value, "list") &&
157
+ Predicate.isFunction(value.list);
162
158
 
163
159
  export const makeClient = <Key, Value, EncodedValue>(
164
160
  definition: KvDefinition<Key, Value, EncodedValue>,
@@ -232,7 +228,8 @@ export const makeClient = <Key, Value, EncodedValue>(
232
228
  key.metadata === undefined
233
229
  ? Option.none<Metadata>()
234
230
  : metadataSchema === undefined
235
- ? Option.some(key.metadata as Metadata)
231
+ ? // SAFETY: without a metadata schema, KV's generic Metadata contract is passed through.
232
+ Option.some(key.metadata as Metadata)
236
233
  : Option.some(yield* S.decodeUnknownEffect(metadataSchema)(key.metadata));
237
234
 
238
235
  keys.push({
@@ -310,6 +307,7 @@ export const Tag =
310
307
 
311
308
  const makeLayer = (binding: LayerOptions) => kvDefinition.layer(tag, binding);
312
309
 
310
+ // SAFETY: the assigned schemas and layer exactly implement TagClass for this KV definition.
313
311
  return Object.assign(tag, {
314
312
  id: kvDefinition.id,
315
313
  keySchema: kvDefinition.key,
package/src/Queue.ts CHANGED
@@ -38,6 +38,7 @@ export class QueueMessageDecodeError extends Data.TaggedError("QueueMessageDecod
38
38
  type RuntimeContext<ROut> = ExecutionContext | WorkerContext | WorkerEnvironment | ROut;
39
39
 
40
40
  type QueueHandlerContext<ROut> = RuntimeContext<ROut> | Scope.Scope;
41
+ type EncodedQueueBody = globalThis.Message<unknown>["body"];
41
42
 
42
43
  export type QueueHandler<ROut, Body = unknown> = (
43
44
  batch: QueueBatch<Body>,
@@ -71,7 +72,7 @@ export const fromMessageBatch = <Body>(
71
72
 
72
73
  export const decodeBatch = <Body>(
73
74
  batch: globalThis.MessageBatch<unknown>,
74
- decodeBody: (body: unknown) => Effect.Effect<Body, unknown>,
75
+ decodeBody: (body: EncodedQueueBody) => Effect.Effect<Body, unknown>,
75
76
  ): Effect.Effect<QueueBatch<Body>, QueueMessageDecodeError> =>
76
77
  Effect.gen(function* () {
77
78
  const messages: Array<QueueMessage<Body>> = [];
@@ -7,7 +7,7 @@ import type {
7
7
  QueueSendOptions as CloudflareQueueSendOptions,
8
8
  QueueSendResponse as CloudflareQueueSendResponse,
9
9
  } from "@cloudflare/workers-types";
10
- import { type Context, Data, Effect, Schema as S } from "effect";
10
+ import { type Context, Data, Effect, Predicate, Schema as S } from "effect";
11
11
 
12
12
  import * as Binding from "./Binding";
13
13
  import type * as RpcDefinition from "./RpcDefinition";
@@ -24,6 +24,8 @@ export type QueueProducer<Body> = Pick<CloudflareQueue<Body>, "send"> &
24
24
 
25
25
  const expectedQueueProducer = "Queue producer binding with send(); optional sendBatch()/metrics()";
26
26
 
27
+ type QueueCandidate = Parameters<typeof Predicate.isUnknown>[0];
28
+
27
29
  export interface QueueBindingDefinition<Message extends RpcDefinition.ServiceFreeSchema> {
28
30
  /** Binding name as configured in `wrangler.jsonc`. */
29
31
  readonly binding: string;
@@ -41,7 +43,7 @@ export interface QueueBindingClient<Message extends RpcDefinition.ServiceFreeSch
41
43
  options?: QueueSendBatchOptions,
42
44
  ) => Effect.Effect<void, QueueOperationError | S.SchemaError>;
43
45
  readonly metrics: () => Effect.Effect<QueueMetrics, QueueOperationError>;
44
- readonly rawUnsafe: Effect.Effect<CloudflareQueue<S.Codec.Encoded<Message>>>;
46
+ readonly rawUnsafe: Effect.Effect<QueueProducer<S.Codec.Encoded<Message>>>;
45
47
  }
46
48
 
47
49
  export class QueueOperationError extends Data.TaggedError("QueueOperationError")<{
@@ -67,15 +69,8 @@ const tryQueuePromise = <A>(
67
69
  }),
68
70
  });
69
71
 
70
- export const isQueue = <Body>(value: unknown): value is QueueProducer<Body> => {
71
- if (typeof value !== "object" || value === null) {
72
- return false;
73
- }
74
-
75
- const resource = value as Record<string, unknown>;
76
-
77
- return typeof resource.send === "function";
78
- };
72
+ export const isQueue = <Body>(value: QueueCandidate): value is QueueProducer<Body> =>
73
+ Predicate.hasProperty(value, "send") && Predicate.isFunction(value.send);
79
74
 
80
75
  export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
81
76
  definition: QueueBindingDefinition<Message>,
@@ -136,7 +131,7 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
136
131
 
137
132
  return tryQueuePromise(definition.binding, "metrics", () => metrics.call(queue));
138
133
  },
139
- rawUnsafe: Effect.succeed(queue as CloudflareQueue<EncodedBody>),
134
+ rawUnsafe: Effect.succeed(queue),
140
135
  });
141
136
  };
142
137