effect-cf 0.26.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 (67) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +8 -6
  3. package/dist/{Artifacts-DcoXcRP5.d.mts → Artifacts-DnHQA-Ax.d.mts} +42 -25
  4. package/dist/Artifacts-rq3hUKQE.mjs +257 -0
  5. package/dist/{Binding-COCtoJXa.d.mts → Binding-xJJJPmiD.d.mts} +6 -5
  6. package/dist/ComputerArtifacts.d.mts +55 -0
  7. package/dist/ComputerArtifacts.mjs +95 -0
  8. package/dist/ComputerWorkspace.d.mts +279 -0
  9. package/dist/ComputerWorkspace.mjs +577 -0
  10. package/dist/ComputerWorkspaceHost.d.mts +1 -1
  11. package/dist/ComputerWorkspaceHost.mjs +18 -16
  12. package/dist/{WorkflowBinding-D5iWzCUa.d.mts → DurableObjectState-B3uFY1eM.d.mts} +90 -141
  13. package/dist/{DurableObjectState-CEh0DVw7.mjs → DurableObjectState-BjkK2kwH.mjs} +4 -49
  14. package/dist/ErrorMessage-BM0K-Nik.mjs +46 -0
  15. package/dist/{Hyperdrive-6p59yyVk.d.mts → Hyperdrive-z-9fFz15.d.mts} +2 -2
  16. package/dist/HyperdrivePg.d.mts +2 -2
  17. package/dist/Vitest.d.mts +6 -4
  18. package/dist/Vitest.mjs +2 -1
  19. package/dist/WorkflowBinding-wZ-yGHow.d.mts +74 -0
  20. package/dist/index.d.mts +92 -398
  21. package/dist/index.mjs +323 -1105
  22. package/package.json +12 -1
  23. package/src/AiGateway.ts +62 -7
  24. package/src/AnalyticsEngine.ts +17 -13
  25. package/src/Artifacts.ts +111 -47
  26. package/src/Binding.ts +34 -23
  27. package/src/BrowserRendering.ts +72 -33
  28. package/src/CloudflareOtlp.ts +8 -5
  29. package/src/ComputerArtifacts.ts +17 -9
  30. package/src/ComputerWorkspace.ts +202 -98
  31. package/src/ComputerWorkspaceHost.ts +55 -27
  32. package/src/ContainerNamespace.ts +8 -9
  33. package/src/D1.ts +8 -14
  34. package/src/DurableObject.ts +27 -11
  35. package/src/DurableObjectAlarm.ts +17 -5
  36. package/src/DurableObjectDefinition.ts +89 -72
  37. package/src/DurableObjectNamespace.ts +26 -12
  38. package/src/DurableObjectRpcWebSocket.ts +9 -10
  39. package/src/DurableObjectState.ts +1 -0
  40. package/src/DurableObjectStorage.ts +5 -2
  41. package/src/DurableObjectWebSocket.ts +4 -1
  42. package/src/Email.ts +26 -25
  43. package/src/Hyperdrive.ts +4 -10
  44. package/src/Images.ts +12 -14
  45. package/src/Kv.ts +15 -17
  46. package/src/Queue.ts +2 -1
  47. package/src/QueueBinding.ts +7 -12
  48. package/src/QueueDefinition.ts +7 -2
  49. package/src/R2.ts +33 -24
  50. package/src/Rpc.ts +15 -21
  51. package/src/RpcDefinition.ts +33 -38
  52. package/src/ServiceBinding.ts +26 -8
  53. package/src/Vectorize.ts +45 -18
  54. package/src/Vitest.ts +9 -3
  55. package/src/WebTransport.ts +11 -5
  56. package/src/Worker.ts +91 -21
  57. package/src/WorkerDefinition.ts +143 -63
  58. package/src/WorkersAi.ts +50 -28
  59. package/src/Workflow.ts +66 -13
  60. package/src/WorkflowBinding.ts +18 -25
  61. package/src/WorkflowDefinition.ts +4 -2
  62. package/src/index.ts +0 -2
  63. package/src/internal/Entrypoint.ts +29 -13
  64. package/src/internal/ErrorMessage.ts +7 -6
  65. package/src/internal/RpcInvocation.ts +34 -11
  66. package/src/internal/Runtime.ts +33 -22
  67. package/src/internal/WorkerContext.ts +3 -3
@@ -29,7 +29,7 @@ export type Handler<ROut, Self extends Definition.Any> = (
29
29
  >;
30
30
 
31
31
  export interface Options<ROut, Self extends Definition.Any> extends Omit<
32
- WorkerEntrypoint.WorkerOptions<ROut, Record<never, never>>,
32
+ WorkerEntrypoint.WorkerOptions<ROut, never, never, Record<never, never>>,
33
33
  "queue" | "rpc"
34
34
  > {
35
35
  readonly queue: Handler<ROut, Self>;
@@ -75,7 +75,11 @@ export interface TagClass<
75
75
  QueueBinding.QueueOperationError,
76
76
  Self
77
77
  >;
78
- readonly rawUnsafe: Effect.Effect<globalThis.Queue<S.Codec.Encoded<Message>>, never, Self>;
78
+ readonly rawUnsafe: Effect.Effect<
79
+ QueueBinding.QueueProducer<S.Codec.Encoded<Message>>,
80
+ never,
81
+ Self
82
+ >;
79
83
  }
80
84
 
81
85
  const makeDefinition = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
@@ -148,6 +152,7 @@ export const Tag =
148
152
 
149
153
  const rawUnsafe = Effect.flatMap(tag, (queue) => queue.rawUnsafe);
150
154
 
155
+ // SAFETY: the assigned definition helpers exactly implement TagClass for this queue schema.
151
156
  return Object.assign(tag, {
152
157
  id: queueDefinition.id,
153
158
  message: queueDefinition.message,
package/src/R2.ts CHANGED
@@ -12,7 +12,7 @@ import type {
12
12
  R2UploadPartOptions as CloudflareR2UploadPartOptions,
13
13
  R2UploadedPart as CloudflareR2UploadedPart,
14
14
  } from "@cloudflare/workers-types";
15
- import { Context, Data, Effect, Option, type Layer } from "effect";
15
+ import { Context, Data, Effect, Option, Predicate, Schema, type Layer } from "effect";
16
16
 
17
17
  import * as Binding from "./Binding";
18
18
  import type { WorkerEnvironment } from "./Environment";
@@ -190,11 +190,11 @@ const isR2ObjectBody = (
190
190
  value: CloudflareR2ObjectBody | CloudflareR2Object,
191
191
  ): value is CloudflareR2ObjectBody =>
192
192
  "body" in value &&
193
- typeof (value as CloudflareR2ObjectBody).arrayBuffer === "function" &&
194
- typeof (value as CloudflareR2ObjectBody).bytes === "function" &&
195
- typeof (value as CloudflareR2ObjectBody).text === "function" &&
196
- typeof (value as CloudflareR2ObjectBody).json === "function" &&
197
- typeof (value as CloudflareR2ObjectBody).blob === "function";
193
+ Predicate.isFunction(value.arrayBuffer) &&
194
+ Predicate.isFunction(value.bytes) &&
195
+ Predicate.isFunction(value.text) &&
196
+ Predicate.isFunction(value.json) &&
197
+ Predicate.isFunction(value.blob);
198
198
 
199
199
  const wrapObjectBody = (binding: string, object: CloudflareR2ObjectBody): R2ObjectBodyClient => ({
200
200
  key: object.key,
@@ -244,23 +244,29 @@ const wrapGetResult = (
244
244
  return wrapObjectBody(binding, object);
245
245
  };
246
246
 
247
- export const isR2Bucket = (value: unknown): value is CloudflareR2Bucket => {
248
- if (typeof value !== "object" || value === null) {
249
- return false;
250
- }
251
-
252
- const resource = value as Record<string, unknown>;
253
-
254
- return (
255
- typeof resource.head === "function" &&
256
- typeof resource.get === "function" &&
257
- typeof resource.put === "function" &&
258
- typeof resource.createMultipartUpload === "function" &&
259
- typeof resource.resumeMultipartUpload === "function" &&
260
- typeof resource.delete === "function" &&
261
- typeof resource.list === "function"
262
- );
263
- };
247
+ type R2BindingValue = Schema.Schema.Type<typeof Schema.Unknown>;
248
+
249
+ const R2BucketSchema = Schema.declare(
250
+ (value: R2BindingValue): value is CloudflareR2Bucket =>
251
+ Predicate.hasProperty(value, "head") &&
252
+ Predicate.isFunction(value.head) &&
253
+ Predicate.hasProperty(value, "get") &&
254
+ Predicate.isFunction(value.get) &&
255
+ Predicate.hasProperty(value, "put") &&
256
+ Predicate.isFunction(value.put) &&
257
+ Predicate.hasProperty(value, "createMultipartUpload") &&
258
+ Predicate.isFunction(value.createMultipartUpload) &&
259
+ Predicate.hasProperty(value, "resumeMultipartUpload") &&
260
+ Predicate.isFunction(value.resumeMultipartUpload) &&
261
+ Predicate.hasProperty(value, "delete") &&
262
+ Predicate.isFunction(value.delete) &&
263
+ Predicate.hasProperty(value, "list") &&
264
+ Predicate.isFunction(value.list),
265
+ );
266
+ const decodeR2Bucket = Schema.decodeUnknownOption(R2BucketSchema);
267
+
268
+ export const isR2Bucket = (value: R2BindingValue): value is CloudflareR2Bucket =>
269
+ Option.isSome(decodeR2Bucket(value));
264
270
 
265
271
  export const makeClient = (
266
272
  definition: R2Definition,
@@ -294,6 +300,7 @@ export const makeClient = (
294
300
  });
295
301
 
296
302
  return (bucket) => {
303
+ // SAFETY: the implementation preserves Cloudflare's get overload distinction for onlyIf options.
297
304
  const get = Effect.fn(
298
305
  "R2.get",
299
306
  spanOptions(definition.binding, "get"),
@@ -314,6 +321,7 @@ export const makeClient = (
314
321
  ),
315
322
  ),
316
323
  get,
324
+ // SAFETY: the implementation preserves Cloudflare's put overload distinction for onlyIf options.
317
325
  put: Effect.fn(
318
326
  "R2.put",
319
327
  spanOptions(definition.binding, "put"),
@@ -352,7 +360,7 @@ export const makeClient = (
352
360
  "R2.delete",
353
361
  spanOptions(definition.binding, "delete"),
354
362
  )((keys: string | ReadonlyArray<string>) => {
355
- const nativeKeys = typeof keys === "string" ? keys : [...keys];
363
+ const nativeKeys = Predicate.isString(keys) ? keys : [...keys];
356
364
 
357
365
  return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
358
366
  }),
@@ -381,6 +389,7 @@ export const Tag =
381
389
 
382
390
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
383
391
 
392
+ // SAFETY: Object.assign attaches exactly the static id/layer members declared by TagClass.
384
393
  return Object.assign(tag, {
385
394
  id,
386
395
  layer: makeLayer,
package/src/Rpc.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RpcStub, RpcTarget } from "cloudflare:workers";
2
- import { Effect, type Scope } from "effect";
2
+ import { Effect, Predicate, Schema, type Scope } from "effect";
3
3
 
4
4
  import { decodeWireError } from "./RpcDefinition";
5
5
 
@@ -57,9 +57,7 @@ export type Stubify<T> = T extends Stubable
57
57
  ? ReadonlyArray<Stubify<Value>>
58
58
  : T extends BaseType
59
59
  ? T
60
- : T extends {
61
- [key: string | number]: unknown;
62
- }
60
+ : T extends Record<string | number, infer _Value>
63
61
  ? {
64
62
  [Key in keyof T]: Stubify<T[Key]>;
65
63
  }
@@ -76,7 +74,7 @@ export type Result<T> = T extends Stubable
76
74
 
77
75
  export type MethodKey<Api> = {
78
76
  [Key in keyof Api]-?: Key extends string
79
- ? Api[Key] extends (...args: Array<any>) => unknown
77
+ ? Api[Key] extends (...args: Array<any>) => any
80
78
  ? Key
81
79
  : never
82
80
  : never;
@@ -84,7 +82,7 @@ export type MethodKey<Api> = {
84
82
 
85
83
  export type MethodArgs<Api, Method extends keyof Api> = Api[Method] extends (
86
84
  ...args: infer Args
87
- ) => unknown
85
+ ) => any
88
86
  ? Args
89
87
  : never;
90
88
 
@@ -98,13 +96,12 @@ export type DisposableValue = {
98
96
  [Symbol.dispose](): void;
99
97
  };
100
98
 
101
- export const isDisposable = (value: unknown): value is DisposableValue =>
102
- (typeof value === "object" || typeof value === "function") &&
103
- value !== null &&
104
- Symbol.dispose in value &&
105
- typeof (value as { readonly [Symbol.dispose]?: unknown })[Symbol.dispose] === "function";
99
+ type RpcBoundaryValue = Schema.Schema.Type<typeof Schema.Unknown>;
100
+
101
+ export const isDisposable = (value: RpcBoundaryValue): value is DisposableValue =>
102
+ Predicate.hasProperty(value, Symbol.dispose) && Predicate.isFunction(value[Symbol.dispose]);
106
103
 
107
- export const dispose = (value: unknown): Effect.Effect<void> =>
104
+ export const dispose = (value: RpcBoundaryValue): Effect.Effect<void> =>
108
105
  Effect.sync(() => {
109
106
  if (isDisposable(value)) {
110
107
  value[Symbol.dispose]();
@@ -112,18 +109,15 @@ export const dispose = (value: unknown): Effect.Effect<void> =>
112
109
  });
113
110
 
114
111
  export const resolve = <A>(value: A): Effect.Effect<Awaited<A>, unknown> =>
115
- isPromiseLike(value)
112
+ Predicate.isPromiseLike(value)
116
113
  ? Effect.tryPromise({
117
- try: () => value,
114
+ try: () => Promise.resolve(value),
118
115
  catch: (cause) => decodeWireError(cause),
119
116
  })
120
- : Effect.sync(() => value as Awaited<A>);
117
+ : Effect.sync(() => {
118
+ // SAFETY: the predicate excluded PromiseLike values, so Awaited<A> is represented by A here.
119
+ return value as Awaited<A>;
120
+ });
121
121
 
122
122
  export const scoped = <A>(value: A): Effect.Effect<Awaited<A>, unknown, Scope.Scope> =>
123
123
  Effect.acquireRelease(resolve(value), (resolved) => dispose(resolved));
124
-
125
- const isPromiseLike = <A>(value: A): value is A & PromiseLike<Awaited<A>> =>
126
- (typeof value === "object" || typeof value === "function") &&
127
- value !== null &&
128
- "then" in value &&
129
- typeof (value as { readonly then?: unknown }).then === "function";
@@ -83,7 +83,9 @@ const WireErrorSchema = S.Union([
83
83
 
84
84
  const wireErrorKey = "effect-cf/RpcDefinition/wireError";
85
85
 
86
- export const isWireError = (error: unknown): error is WireError =>
86
+ type WireValue = S.Schema.Type<typeof S.Unknown>;
87
+
88
+ export const isWireError = (error: WireValue): error is WireError =>
87
89
  error instanceof RpcArgumentCountError ||
88
90
  error instanceof RpcArgumentDecodeError ||
89
91
  error instanceof RpcSuccessEncodeError;
@@ -92,7 +94,7 @@ export const isWireError = (error: unknown): error is WireError =>
92
94
  * Encodes package RPC errors into a plain `Error` envelope so their tags
93
95
  * survive Cloudflare RPC serialization, which drops custom error properties.
94
96
  */
95
- export const encodeWireError = (error: unknown): unknown => {
97
+ export const encodeWireError = (error: WireValue): WireValue => {
96
98
  if (!isWireError(error)) {
97
99
  return error;
98
100
  }
@@ -105,13 +107,13 @@ export const encodeWireError = (error: unknown): unknown => {
105
107
  };
106
108
 
107
109
  /** Restores package RPC errors from the {@link encodeWireError} envelope. */
108
- export const decodeWireError = (cause: unknown): unknown => {
110
+ export const decodeWireError = (cause: WireValue): WireValue => {
109
111
  if (!(cause instanceof Error)) {
110
112
  return cause;
111
113
  }
112
114
 
113
115
  try {
114
- const parsed: unknown = JSON.parse(cause.message);
116
+ const parsed: WireValue = S.decodeUnknownSync(S.Unknown)(JSON.parse(cause.message));
115
117
 
116
118
  if (!Predicate.hasProperty(parsed, wireErrorKey)) {
117
119
  return cause;
@@ -193,19 +195,22 @@ export namespace Method {
193
195
 
194
196
  export type Success<Self extends Any> = S.Schema.Type<Self["success"]>;
195
197
 
196
- export type EncodedSuccess<Self extends Any> = S.Json;
198
+ export type EncodedSuccess<_Self extends Any> = S.Json;
197
199
  }
198
200
 
199
201
  export type Methods = Record<string, Method.Any>;
200
202
 
201
203
  export type NoReservedMethods<
202
- MethodsShape extends Methods,
204
+ MethodDefinitions extends Methods,
203
205
  Reserved extends string = ReservedMethodName,
204
- > = Extract<keyof MethodsShape, Reserved> extends never ? MethodsShape : never;
206
+ > = Extract<keyof MethodDefinitions, Reserved> extends never ? MethodDefinitions : never;
205
207
 
206
- export interface Definition<Id extends string = string, MethodsShape extends Methods = Methods> {
208
+ export interface Definition<
209
+ Id extends string = string,
210
+ MethodDefinitions extends Methods = Methods,
211
+ > {
207
212
  readonly id: Id;
208
- readonly methods: MethodsShape;
213
+ readonly methods: MethodDefinitions;
209
214
  }
210
215
 
211
216
  export namespace Definition {
@@ -225,9 +230,13 @@ export namespace Definition {
225
230
  export type MethodNames<Self extends Any> = Extract<keyof Self["methods"], string>;
226
231
  }
227
232
 
228
- export const assertNoReservedMethods = (
233
+ type ReservedMethodValue = S.Schema.Type<typeof S.Unknown>;
234
+
235
+ export const assertNoReservedMethods = <
236
+ MethodDefinitions extends Readonly<Record<string, ReservedMethodValue>>,
237
+ >(
229
238
  target: string,
230
- methods: Record<string, unknown>,
239
+ methods: MethodDefinitions,
231
240
  reserved: ReadonlySet<string>,
232
241
  ) => {
233
242
  for (const method of Object.keys(methods)) {
@@ -276,11 +285,8 @@ export const decodeArgs = Effect.fnUntraced(function* <
276
285
  });
277
286
  }
278
287
 
279
- const decoded = yield* (
280
- S.decodeUnknownEffect(wireCodec(S.Tuple(methodDefinition.args)))(args) as Effect.Effect<
281
- unknown,
282
- unknown
283
- >
288
+ const decoded = yield* S.decodeUnknownEffect(wireCodec(S.Tuple(methodDefinition.args)))(
289
+ args,
284
290
  ).pipe(
285
291
  Effect.mapError(
286
292
  (cause) =>
@@ -292,6 +298,7 @@ export const decodeArgs = Effect.fnUntraced(function* <
292
298
  ),
293
299
  );
294
300
 
301
+ // SAFETY: the tuple codec is constructed from this exact method's argument schemas.
295
302
  return decoded as Method.Args<Self["methods"][MethodName]>;
296
303
  });
297
304
 
@@ -317,11 +324,8 @@ export const encodeArgs = Effect.fnUntraced(function* <
317
324
  });
318
325
  }
319
326
 
320
- const encoded = yield* (
321
- S.encodeUnknownEffect(wireCodec(S.Tuple(methodDefinition.args)))(args) as Effect.Effect<
322
- unknown,
323
- unknown
324
- >
327
+ const encoded = yield* S.encodeUnknownEffect(wireCodec(S.Tuple(methodDefinition.args)))(
328
+ args,
325
329
  ).pipe(
326
330
  Effect.mapError(
327
331
  (cause) =>
@@ -333,6 +337,7 @@ export const encodeArgs = Effect.fnUntraced(function* <
333
337
  ),
334
338
  );
335
339
 
340
+ // SAFETY: the tuple codec is constructed from this exact method's argument schemas.
336
341
  return encoded as Method.EncodedArgs<Self["methods"][MethodName]>;
337
342
  });
338
343
 
@@ -346,12 +351,7 @@ export const encodeSuccess = <
346
351
  ): Effect.Effect<Method.EncodedSuccess<Self["methods"][MethodName]>, RpcSuccessEncodeError> => {
347
352
  const methodDefinition = definition.methods[methodName];
348
353
 
349
- return (
350
- S.encodeUnknownEffect(wireCodec(methodDefinition.success))(value) as Effect.Effect<
351
- Method.EncodedSuccess<Self["methods"][MethodName]>,
352
- unknown
353
- >
354
- ).pipe(
354
+ return S.encodeUnknownEffect(wireCodec(methodDefinition.success))(value).pipe(
355
355
  Effect.mapError(
356
356
  (cause) =>
357
357
  new RpcSuccessEncodeError({
@@ -369,16 +369,11 @@ export const decodeSuccess = <
369
369
  >(
370
370
  definition: Self,
371
371
  methodName: MethodName,
372
- value: unknown,
372
+ value: WireValue,
373
373
  ): Effect.Effect<Method.Success<Self["methods"][MethodName]>, RpcSuccessDecodeError> => {
374
374
  const methodDefinition = definition.methods[methodName];
375
375
 
376
- return (
377
- S.decodeUnknownEffect(wireCodec(methodDefinition.success))(value) as Effect.Effect<
378
- Method.Success<Self["methods"][MethodName]>,
379
- unknown
380
- >
381
- ).pipe(
376
+ return S.decodeUnknownEffect(wireCodec(methodDefinition.success))(value).pipe(
382
377
  Effect.mapError(
383
378
  (cause) =>
384
379
  new RpcSuccessDecodeError({
@@ -390,11 +385,11 @@ export const decodeSuccess = <
390
385
  );
391
386
  };
392
387
 
393
- export const make = <Id extends string, const MethodsShape extends Methods>(
388
+ export const make = <Id extends string, const MethodDefinitions extends Methods>(
394
389
  id: Id,
395
- methods: MethodsShape,
396
- ): Definition<Id, MethodsShape> => {
390
+ methods: MethodDefinitions,
391
+ ): Definition<Id, MethodDefinitions> => {
397
392
  assertNoReservedMethods(id, methods, reservedMethodNames);
398
393
 
399
- return { id, methods } as Definition<Id, MethodsShape>;
394
+ return { id, methods };
400
395
  };
@@ -1,4 +1,4 @@
1
- import { Data, Effect, type Context, type Scope } from "effect";
1
+ import { Data, Effect, Option, Predicate, Schema, type Context, type Scope } from "effect";
2
2
 
3
3
  import * as Binding from "./Binding";
4
4
  import * as CloudflareRpc from "./Rpc";
@@ -19,7 +19,7 @@ export interface ServiceFetcher {
19
19
 
20
20
  type RpcClient<Api> = {
21
21
  readonly [Key in keyof Api as Key extends string
22
- ? Api[Key] extends (...args: Array<any>) => unknown
22
+ ? Api[Key] extends (...args: Array<any>) => any
23
23
  ? Key
24
24
  : never
25
25
  : never]: Api[Key];
@@ -232,10 +232,17 @@ export type ServiceBindingStaticClient<
232
232
  ) => Effect.Effect<Awaited<ServiceMethodSuccess<Api, Method>>, unknown, Scope.Scope | R>;
233
233
  };
234
234
 
235
+ type ServiceBindingValue = Schema.Schema.Type<typeof Schema.Unknown>;
236
+
237
+ const ServiceBindingSchema = Schema.declare(
238
+ (value: ServiceBindingValue): value is ServiceFetcher =>
239
+ Predicate.hasProperty(value, "fetch") && Predicate.isFunction(value.fetch),
240
+ );
241
+ const decodeServiceBinding = Schema.decodeUnknownOption(ServiceBindingSchema);
242
+
235
243
  export const isServiceBindingClient = <Api extends object>(
236
- value: unknown,
237
- ): value is ServiceBindingClient<Api> =>
238
- typeof value === "object" && value !== null && typeof Reflect.get(value, "fetch") === "function";
244
+ value: ServiceBindingValue,
245
+ ): value is ServiceBindingClient<Api> => Option.isSome(decodeServiceBinding(value));
239
246
 
240
247
  export const makeClient = <
241
248
  Api extends object,
@@ -259,6 +266,7 @@ export const makeClient = <
259
266
  ...args: ServiceMethodArgs<Api, Method>
260
267
  ) {
261
268
  const methodName = String(method);
269
+ // SAFETY: definition-backed clients derive Method and Args from this exact RPC definition.
262
270
  const encodedArgs =
263
271
  definition.definition === undefined
264
272
  ? args
@@ -277,6 +285,7 @@ export const makeClient = <
277
285
  ),
278
286
  );
279
287
 
288
+ // SAFETY: encoding preserves the selected API method's positional argument contract.
280
289
  return yield* RpcInvocation.invokeRpcMethod(
281
290
  service,
282
291
  method,
@@ -295,9 +304,11 @@ export const makeClient = <
295
304
  value: Awaited<ServiceMethodCloudflareReturn<Api, Method>>,
296
305
  ) {
297
306
  if (definition.definition === undefined) {
307
+ // SAFETY: without a definition the resolved native RPC value is the API method's declared success.
298
308
  return value as ServiceMethodSuccess<Api, Method>;
299
309
  }
300
310
 
311
+ // SAFETY: definition-backed clients derive the method name from this exact RPC definition.
301
312
  const decoded = yield* RpcDefinition.decodeSuccess(
302
313
  definition.definition,
303
314
  methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
@@ -313,6 +324,7 @@ export const makeClient = <
313
324
  ),
314
325
  );
315
326
 
327
+ // SAFETY: decodeSuccess validates against the selected method's declared success schema.
316
328
  return decoded as ServiceMethodSuccess<Api, Method>;
317
329
  });
318
330
 
@@ -347,6 +359,7 @@ export const makeClient = <
347
359
 
348
360
  const directMethods = makeDirectMethods<never, Api, Definition>(definition.definition, call);
349
361
 
362
+ // SAFETY: Object.assign attaches the four client operations required by ServiceBindingEffectClient.
350
363
  return Object.assign(directMethods, {
351
364
  fetch,
352
365
  rpc,
@@ -457,6 +470,7 @@ export const Service =
457
470
  call,
458
471
  );
459
472
 
473
+ // SAFETY: Object.assign attaches the definition-derived service metadata and static operations.
460
474
  return Object.assign(tag, directMethods, {
461
475
  [TypeId]: TypeId,
462
476
  definition,
@@ -483,14 +497,18 @@ export const makeDirectMethods = <
483
497
  rpcDefinition: Definition | undefined,
484
498
  call: ServiceCall<R, Api>,
485
499
  ): DirectMethods<R, Definition> => {
486
- const methods = {} as Record<string, unknown>;
500
+ type DynamicMethod = (...args: Array<any>) => Effect.Effect<any, ServiceBindingRpcError, R>;
501
+ const methods: Record<string, DynamicMethod> = {};
487
502
 
488
503
  if (rpcDefinition !== undefined) {
504
+ // SAFETY: rpcDefinition limits these names and tuples to the same definition used to construct call.
505
+ const invoke = call as (method: string, ...args: Array<any>) => ReturnType<DynamicMethod>;
506
+
489
507
  for (const methodName of Object.keys(rpcDefinition.methods)) {
490
- methods[methodName] = (...args: Array<unknown>) =>
491
- (call as (method: string, ...args: Array<unknown>) => unknown)(methodName, ...args);
508
+ methods[methodName] = (...args: Array<any>) => invoke(methodName, ...args);
492
509
  }
493
510
  }
494
511
 
512
+ // SAFETY: every declared definition method is populated above with a forwarding Effect function.
495
513
  return methods as DirectMethods<R, Definition>;
496
514
  };
package/src/Vectorize.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Data, Effect, type Layer } from "effect";
1
+ import { Context, Data, Effect, Option, Predicate, Schema, type Layer } from "effect";
2
2
  import type {
3
3
  Vectorize as CloudflareVectorize,
4
4
  VectorizeAsyncMutation as CloudflareVectorizeAsyncMutation,
@@ -157,22 +157,43 @@ const spanOptions = (binding: string, operation: VectorizeOperation) => ({
157
157
  attributes: { binding, operation },
158
158
  });
159
159
 
160
- const hasFunction = (value: object, key: string): boolean =>
161
- typeof Reflect.get(value, key) === "function";
160
+ type VectorizeBindingValue = Schema.Schema.Type<typeof Schema.Unknown>;
162
161
 
163
- export const isVectorizeBinding = (value: unknown): value is VectorizeBinding =>
164
- typeof value === "object" &&
165
- value !== null &&
166
- hasFunction(value, "describe") &&
167
- hasFunction(value, "query") &&
168
- hasFunction(value, "insert") &&
169
- hasFunction(value, "upsert") &&
170
- hasFunction(value, "deleteByIds") &&
171
- hasFunction(value, "getByIds");
162
+ const invokeVectorizeMethod = <Args extends Array<any>, Return>(
163
+ method: (...args: Args) => Return,
164
+ receiver: VectorizeRuntimeBinding,
165
+ args: Args,
166
+ ): Return => {
167
+ const result: VectorizeBindingValue = Function.prototype.apply.call(method, receiver, args);
168
+
169
+ // SAFETY: the native apply intrinsic returns method's declared Return without reading proxy properties.
170
+ return result as Return;
171
+ };
172
+
173
+ const VectorizeBindingSchema = Schema.declare(
174
+ (value: VectorizeBindingValue): value is VectorizeBinding =>
175
+ Predicate.hasProperty(value, "describe") &&
176
+ Predicate.isFunction(value.describe) &&
177
+ Predicate.hasProperty(value, "query") &&
178
+ Predicate.isFunction(value.query) &&
179
+ Predicate.hasProperty(value, "insert") &&
180
+ Predicate.isFunction(value.insert) &&
181
+ Predicate.hasProperty(value, "upsert") &&
182
+ Predicate.isFunction(value.upsert) &&
183
+ Predicate.hasProperty(value, "deleteByIds") &&
184
+ Predicate.isFunction(value.deleteByIds) &&
185
+ Predicate.hasProperty(value, "getByIds") &&
186
+ Predicate.isFunction(value.getByIds),
187
+ );
188
+ const decodeVectorizeBinding = Schema.decodeUnknownOption(VectorizeBindingSchema);
189
+
190
+ export const isVectorizeBinding = (value: VectorizeBindingValue): value is VectorizeBinding =>
191
+ Option.isSome(decodeVectorizeBinding(value));
172
192
 
173
193
  export const makeClient =
174
194
  (definition: VectorizeDefinition) =>
175
195
  (index: VectorizeBinding): VectorizeClient => {
196
+ // SAFETY: isVectorizeBinding validates every required runtime method before makeClient is called.
176
197
  const runtime = index as VectorizeRuntimeBinding;
177
198
  const deleteByIds = Effect.fn(
178
199
  "Vectorize.deleteByIds",
@@ -190,18 +211,23 @@ export const makeClient =
190
211
  "Vectorize.query",
191
212
  spanOptions(definition.binding, "query"),
192
213
  )((vector: VectorizeValues, options?: VectorizeQueryOptions) =>
193
- tryVectorizePromise(definition.binding, "query", () =>
194
- runtime.query(vector as Float32Array | Float64Array | number[], options),
195
- ),
214
+ tryVectorizePromise(definition.binding, "query", () => {
215
+ // SAFETY: Cloudflare accepts these vector containers without mutating the readonly array variant.
216
+ return runtime.query(vector as Float32Array | Float64Array | number[], options);
217
+ }),
196
218
  ),
197
219
  queryById: Effect.fn(
198
220
  "Vectorize.queryById",
199
221
  spanOptions(definition.binding, "queryById"),
200
222
  )((vectorId: string, options?: VectorizeQueryOptions) =>
201
223
  runtime.queryById !== undefined
202
- ? tryVectorizePromise(definition.binding, "queryById", () =>
203
- runtime.queryById!(vectorId, options),
204
- )
224
+ ? tryVectorizePromise(definition.binding, "queryById", () => {
225
+ const queryById = runtime.queryById;
226
+
227
+ return queryById === undefined
228
+ ? Promise.reject(new TypeError("Vectorize binding does not expose queryById()"))
229
+ : invokeVectorizeMethod(queryById, runtime, [vectorId, options]);
230
+ })
205
231
  : Effect.fail(
206
232
  vectorizeError(
207
233
  definition.binding,
@@ -251,6 +277,7 @@ export const Tag =
251
277
 
252
278
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
253
279
 
280
+ // SAFETY: Object.assign attaches exactly the static id/layer members declared by TagClass.
254
281
  return Object.assign(tag, {
255
282
  id,
256
283
  layer: makeLayer,
package/src/Vitest.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  runInDurableObject as runInDurableObjectPromise,
25
25
  waitOnExecutionContext,
26
26
  } from "cloudflare:test";
27
- import { ConfigProvider, Effect, Exit, Layer } from "effect";
27
+ import { ConfigProvider, Effect, Exit, Layer, Schema } from "effect";
28
28
 
29
29
  import {
30
30
  DurableObjectState,
@@ -148,6 +148,7 @@ export const pages = Effect.fn("effect-cf/vitest/pages")(function* <
148
148
  >(handler: Function, init: PagesEventContextInit<Function>) {
149
149
  return yield* Effect.acquireUseRelease(
150
150
  Effect.sync(() =>
151
+ // SAFETY: PagesEventContextInit computes the same conditional initializer accepted by this Function.
151
152
  createPagesEventContext<Function>(
152
153
  init as Parameters<typeof createPagesEventContext<Function>>[0],
153
154
  ),
@@ -362,7 +363,10 @@ export interface WorkflowMockEvent {
362
363
  export interface WorkflowInstanceModifier {
363
364
  readonly disableSleeps: (steps?: ReadonlyArray<WorkflowStepTarget>) => Effect.Effect<void>;
364
365
  readonly disableRetryDelays: (steps?: ReadonlyArray<WorkflowStepTarget>) => Effect.Effect<void>;
365
- readonly mockStepResult: (step: WorkflowStepTarget, result: unknown) => Effect.Effect<void>;
366
+ readonly mockStepResult: (
367
+ step: WorkflowStepTarget,
368
+ result: WorkflowMockResult,
369
+ ) => Effect.Effect<void>;
366
370
  readonly mockStepError: (
367
371
  step: WorkflowStepTarget,
368
372
  error: Error,
@@ -373,6 +377,8 @@ export interface WorkflowInstanceModifier {
373
377
  readonly forceEventTimeout: (step: WorkflowStepTarget) => Effect.Effect<void>;
374
378
  }
375
379
 
380
+ type WorkflowMockResult = Schema.Schema.Type<typeof Schema.Unknown>;
381
+
376
382
  /** Effect-native introspector for one Workflow instance. */
377
383
  export interface WorkflowInstanceIntrospector {
378
384
  readonly raw: NativeWorkflowInstanceIntrospector;
@@ -413,7 +419,7 @@ const wrapWorkflowModifier = (
413
419
  });
414
420
 
415
421
  const runWorkflowModifier = Effect.fnUntraced(function* <A, E, R>(
416
- register: (use: (modifier: NativeWorkflowInstanceModifier) => Promise<void>) => Promise<unknown>,
422
+ register: (use: (modifier: NativeWorkflowInstanceModifier) => Promise<void>) => Promise<any>,
417
423
  use: (modifier: WorkflowInstanceModifier) => Effect.Effect<A, E, R>,
418
424
  ): Effect.fn.Return<A, E, R> {
419
425
  const context = yield* Effect.context<R>();
@@ -30,7 +30,7 @@
30
30
  * the edge does provide.
31
31
  */
32
32
  import type { Request as CloudflareRequest } from "@cloudflare/workers-types";
33
- import { Data, Effect, Option, Result, Schema as S } from "effect";
33
+ import { Data, Effect, Option, Predicate, Result, Schema as S } from "effect";
34
34
 
35
35
  /**
36
36
  * Metadata Cloudflare's edge reports about the inbound client connection.
@@ -92,10 +92,16 @@ export interface Capabilities {
92
92
  }
93
93
 
94
94
  /** Feature-detects the WebTransport capabilities of the current runtime. */
95
- export const capabilities: Effect.Effect<Capabilities> = Effect.sync(() => ({
96
- inboundSessions: false,
97
- outboundSessions: typeof (globalThis as Record<string, unknown>)["WebTransport"] === "function",
98
- }));
95
+ export const capabilities: Effect.Effect<Capabilities> = Effect.sync(() => {
96
+ const runtime = globalThis;
97
+
98
+ return {
99
+ inboundSessions: false,
100
+ outboundSessions:
101
+ Predicate.hasProperty(runtime, "WebTransport") &&
102
+ Predicate.isFunction(runtime["WebTransport"]),
103
+ };
104
+ });
99
105
 
100
106
  /** WebTransport capability missing from the current Workers runtime. */
101
107
  export type WebTransportCapability = "inbound-sessions" | "outbound-sessions";