effect-cf 0.27.0 → 0.28.1

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 +18 -0
  2. package/README.md +6 -0
  3. package/dist/{Artifacts-DcoXcRP5.d.mts → Artifacts-DnHQA-Ax.d.mts} +42 -25
  4. package/dist/{Artifacts-BhWrdyqX.mjs → Artifacts-rq3hUKQE.mjs} +46 -30
  5. package/dist/{Binding-COCtoJXa.d.mts → Binding-xJJJPmiD.d.mts} +6 -5
  6. package/dist/ComputerArtifacts.d.mts +1 -1
  7. package/dist/ComputerArtifacts.mjs +9 -9
  8. package/dist/ComputerWorkspace.d.mts +5 -5
  9. package/dist/ComputerWorkspace.mjs +107 -75
  10. package/dist/ComputerWorkspaceHost.d.mts +1 -1
  11. package/dist/ComputerWorkspaceHost.mjs +18 -16
  12. package/dist/{DurableObjectState-Dux8KfjG.mjs → DurableObjectState-BjkK2kwH.mjs} +3 -3
  13. package/dist/{DurableObjectState-biHrHwrh.d.mts → DurableObjectState-OupNMMUl.d.mts} +89 -64
  14. package/dist/{ErrorMessage-_lg53p3W.mjs → ErrorMessage-BM0K-Nik.mjs} +1 -4
  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 -5
  18. package/dist/Vitest.mjs +2 -2
  19. package/dist/{WorkflowBinding-zlRPCXVf.d.mts → WorkflowBinding-DUZtKCVM.d.mts} +5 -10
  20. package/dist/index.d.mts +88 -62
  21. package/dist/index.mjs +291 -181
  22. package/package.json +1 -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 +21 -11
  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 +33 -13
  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 +97 -24
  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/internal/Entrypoint.ts +29 -13
  63. package/src/internal/ErrorMessage.ts +7 -6
  64. package/src/internal/RpcInvocation.ts +34 -11
  65. package/src/internal/Runtime.ts +33 -22
  66. package/src/internal/Telemetry.ts +17 -10
  67. package/src/internal/WorkerContext.ts +3 -3
package/src/WorkersAi.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Data, Effect, type Layer } from "effect";
1
+ import { Context, Data, Effect, Option, Predicate, Schema as S, type Layer } from "effect";
2
2
  import type {
3
3
  Ai as CloudflareAi,
4
4
  AiAsyncBatchResponse,
@@ -44,11 +44,13 @@ export type WorkersAiAsyncBatchResponse = AiAsyncBatchResponse;
44
44
  export type WorkersAiEmbeddingInput =
45
45
  | AiTextEmbeddingsInput
46
46
  | Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input
47
- | Record<string, unknown>;
47
+ | Parameters<WorkersAiBinding["run"]>[1];
48
+
49
+ const embeddingDimensionsKey = "shape";
48
50
 
49
51
  export interface WorkersAiEmbeddingResponse {
50
52
  readonly data: ReadonlyArray<ReadonlyArray<number>>;
51
- readonly shape: ReadonlyArray<number>;
53
+ readonly [embeddingDimensionsKey]: ReadonlyArray<number>;
52
54
  }
53
55
 
54
56
  export interface WorkersAiClient<ModelList extends AiModelListType = AiModels> {
@@ -89,7 +91,10 @@ export interface WorkersAiClient<ModelList extends AiModelListType = AiModels> {
89
91
  input: ModelList[Name]["inputs"],
90
92
  options?: WorkersAiOptions,
91
93
  ): Effect.Effect<ModelList[Name]["postProcessedOutputs"], WorkersAiOperationError>;
92
- <Input extends Record<string, unknown>, Output = Record<string, unknown>>(
94
+ <
95
+ Input extends Parameters<WorkersAiBinding["run"]>[1],
96
+ Output = Awaited<ReturnType<WorkersAiBinding["run"]>>,
97
+ >(
93
98
  model: string,
94
99
  input: Input,
95
100
  options?: WorkersAiOptions,
@@ -161,33 +166,47 @@ const tryWorkersAiSync = <A>(
161
166
  catch: (cause) => workersAiError(binding, operation, cause),
162
167
  });
163
168
 
164
- const hasFunction = (value: object, key: string): boolean =>
165
- typeof Reflect.get(value, key) === "function";
169
+ type WorkersAiBindingValue = S.Schema.Type<typeof S.Unknown>;
170
+
171
+ const WorkersAiBindingSchema = S.declare(
172
+ (value: WorkersAiBindingValue): value is WorkersAiBinding =>
173
+ Predicate.hasProperty(value, "run") &&
174
+ Predicate.isFunction(value.run) &&
175
+ Predicate.hasProperty(value, "gateway") &&
176
+ Predicate.isFunction(value.gateway) &&
177
+ Predicate.hasProperty(value, "models") &&
178
+ Predicate.isFunction(value.models),
179
+ );
180
+ const decodeWorkersAiBinding = S.decodeUnknownOption(WorkersAiBindingSchema);
166
181
 
167
- export const isWorkersAiBinding = (value: unknown): value is WorkersAiBinding =>
168
- typeof value === "object" &&
169
- value !== null &&
170
- hasFunction(value, "run") &&
171
- hasFunction(value, "gateway") &&
172
- hasFunction(value, "models");
182
+ export const isWorkersAiBinding = (value: WorkersAiBindingValue): value is WorkersAiBinding =>
183
+ Option.isSome(decodeWorkersAiBinding(value));
173
184
 
174
185
  export const embeddingResponse = (value: {
175
186
  readonly data?: ReadonlyArray<ReadonlyArray<number>>;
176
- readonly shape?: ReadonlyArray<number>;
187
+ readonly [embeddingDimensionsKey]?: ReadonlyArray<number>;
177
188
  }): WorkersAiEmbeddingResponse => ({
178
189
  data: value.data ?? [],
179
- shape: value.shape ?? [],
190
+ [embeddingDimensionsKey]: value[embeddingDimensionsKey] ?? [],
180
191
  });
181
192
 
193
+ const decodeEmbeddingResponse = S.decodeUnknownOption(
194
+ S.Struct({
195
+ data: S.optional(S.Array(S.Array(S.Number))),
196
+ [embeddingDimensionsKey]: S.optional(S.Array(S.Number)),
197
+ }),
198
+ );
199
+
182
200
  export const makeClient =
183
201
  <ModelList extends AiModelListType = AiModels>(definition: WorkersAiDefinition) =>
184
202
  (ai: WorkersAiBinding<ModelList>): WorkersAiClient<ModelList> => {
185
203
  const run = Effect.fn("WorkersAi.run")(
186
- (model: string, input: Record<string, unknown>, options?: WorkersAiOptions) =>
187
- tryWorkersAiPromise(definition.binding, "run", () =>
188
- ai.run(model as string & {}, input, options),
189
- ),
190
- ) as WorkersAiClient<ModelList>["run"];
204
+ (model: string, input: Parameters<WorkersAiBinding["run"]>[1], options?: WorkersAiOptions) =>
205
+ tryWorkersAiPromise(definition.binding, "run", () => {
206
+ // SAFETY: a non-literal string intentionally selects Cloudflare's unknown-model fallback overload.
207
+ return ai.run(model as string & {}, input, options);
208
+ }),
209
+ );
191
210
 
192
211
  return {
193
212
  definition,
@@ -198,17 +217,19 @@ export const makeClient =
198
217
  ),
199
218
  run,
200
219
  runEmbedding: Effect.fn("WorkersAi.runEmbedding")(
201
- (model: string, input: WorkersAiEmbeddingInput, options?: WorkersAiOptions) =>
202
- run(model, input as Record<string, unknown>, options).pipe(
220
+ (model: string, input: WorkersAiEmbeddingInput, options?: WorkersAiOptions) => {
221
+ // SAFETY: every WorkersAiEmbeddingInput variant is a string-keyed Workers AI payload.
222
+ const modelInput = input as Parameters<WorkersAiBinding["run"]>[1];
223
+
224
+ return run(model, modelInput, options).pipe(
203
225
  Effect.map((response) =>
204
- embeddingResponse(
205
- response as {
206
- readonly data?: ReadonlyArray<ReadonlyArray<number>>;
207
- readonly shape?: ReadonlyArray<number>;
208
- },
209
- ),
226
+ Option.match(decodeEmbeddingResponse(response), {
227
+ onNone: () => embeddingResponse({}),
228
+ onSome: embeddingResponse,
229
+ }),
210
230
  ),
211
- ),
231
+ );
232
+ },
212
233
  ),
213
234
  models: Effect.fn("WorkersAi.models")((params?: WorkersAiModelsSearchParams) =>
214
235
  tryWorkersAiPromise(definition.binding, "models", () => ai.models(params)),
@@ -242,6 +263,7 @@ export const Tag =
242
263
 
243
264
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
244
265
 
266
+ // SAFETY: Object.assign attaches exactly the static id/layer members declared by TagClass.
245
267
  return Object.assign(tag, {
246
268
  id,
247
269
  layer: makeLayer,
package/src/Workflow.ts CHANGED
@@ -12,7 +12,17 @@ import {
12
12
  type WorkflowStepEvent,
13
13
  type WorkflowTimeoutDuration,
14
14
  } from "cloudflare:workers";
15
- import { Context, Data, Effect, Layer, type ManagedRuntime, type Scope } from "effect";
15
+ import { NonRetryableError as CloudflareNonRetryableError } from "cloudflare:workflows";
16
+ import {
17
+ Cause,
18
+ Context,
19
+ Data,
20
+ Effect,
21
+ Layer,
22
+ type ManagedRuntime,
23
+ Option,
24
+ type Scope,
25
+ } from "effect";
16
26
 
17
27
  import { WorkerEnvironment, type WorkerEnv } from "./Environment";
18
28
  import { ExecutionContext, WorkerContext } from "./Worker";
@@ -49,6 +59,22 @@ export class WorkflowStepError extends Data.TaggedError("WorkflowStepError")<{
49
59
  }
50
60
  }
51
61
 
62
+ /**
63
+ * Typed step failure that Cloudflare must treat as terminal instead of retrying.
64
+ *
65
+ * The workflow boundary converts this value into Cloudflare's native
66
+ * `NonRetryableError` while preserving the original cause and its message.
67
+ */
68
+ export class WorkflowStepNonRetryableError extends Data.TaggedError(
69
+ "WorkflowStepNonRetryableError",
70
+ )<{
71
+ readonly cause: unknown;
72
+ }> {
73
+ override get message(): string {
74
+ return `Workflow step failed without retry: ${ErrorMessage.causeMessage(this.cause)}`;
75
+ }
76
+ }
77
+
52
78
  export interface WorkflowStepService {
53
79
  readonly raw: CloudflareWorkflowStep;
54
80
  do<A, E, R>(
@@ -85,6 +111,7 @@ export class WorkflowStepContext extends Context.Service<
85
111
  const fromWorkflowEvent = <Payload>(
86
112
  event: CloudflareWorkflowEvent<Payload>,
87
113
  ): WorkflowEventService<Payload> => ({
114
+ // SAFETY: raw intentionally erases only the payload type while retaining the same event object.
88
115
  raw: event as CloudflareWorkflowEvent<unknown>,
89
116
  payload: event.payload,
90
117
  timestamp: event.timestamp,
@@ -92,6 +119,27 @@ const fromWorkflowEvent = <Payload>(
92
119
  workflowName: event.workflowName,
93
120
  });
94
121
 
122
+ const toCloudflareNonRetryableError = (
123
+ error: WorkflowStepNonRetryableError,
124
+ ): CloudflareNonRetryableError => {
125
+ const cloudflareError = new CloudflareNonRetryableError(error.message);
126
+
127
+ cloudflareError.cause = error.cause;
128
+
129
+ return cloudflareError;
130
+ };
131
+
132
+ const exposeCloudflareNonRetryableError = <A, E, R>(
133
+ effect: Effect.Effect<A, E, R>,
134
+ ): Effect.Effect<A, E, R> =>
135
+ Effect.catchCause(effect, (cause) => {
136
+ const error = Cause.findErrorOption(cause);
137
+
138
+ return Option.isSome(error) && error.value instanceof WorkflowStepNonRetryableError
139
+ ? Effect.die(toCloudflareNonRetryableError(error.value))
140
+ : Effect.failCause(cause);
141
+ });
142
+
95
143
  const fromWorkflowStep = (
96
144
  step: CloudflareWorkflowStep,
97
145
  runPromise: RunWorkflowStepEffect,
@@ -104,17 +152,21 @@ const fromWorkflowStep = (
104
152
  try: () => {
105
153
  const run = (stepContext: CloudflareWorkflowStepContext) =>
106
154
  runPromise(
107
- Effect.scoped(
108
- Effect.provideService(
109
- Effect.provideContext(
110
- effect as Effect.Effect<A, E, Exclude<R, WorkflowStepContext>>,
111
- context,
155
+ exposeCloudflareNonRetryableError(
156
+ Effect.scoped(
157
+ Effect.provideService(
158
+ Effect.provideContext(
159
+ // SAFETY: WorkflowStepContext is provided immediately below; context supplies every other R service.
160
+ effect as Effect.Effect<A, E, Exclude<R, WorkflowStepContext>>,
161
+ context,
162
+ ),
163
+ WorkflowStepContext,
164
+ stepContext,
112
165
  ),
113
- WorkflowStepContext,
114
- stepContext,
115
166
  ),
116
167
  ),
117
168
  );
169
+ // SAFETY: these overloads reproduce the Cloudflare WorkflowStep.do runtime signatures used here.
118
170
  const rawStep = step as {
119
171
  do(
120
172
  name: string,
@@ -132,7 +184,7 @@ const fromWorkflowStep = (
132
184
  catch: (cause) => new WorkflowStepError({ step: name, operation: "do", cause }),
133
185
  }),
134
186
  ),
135
- ) as Effect.Effect<A, WorkflowStepError, Exclude<R, WorkflowStepContext>>,
187
+ ),
136
188
  sleep: (name, duration) =>
137
189
  Effect.tryPromise({
138
190
  try: () => step.sleep(name, duration),
@@ -151,7 +203,10 @@ const fromWorkflowStep = (
151
203
  },
152
204
  ) =>
153
205
  Effect.tryPromise({
154
- try: () => step.waitForEvent(name, options) as Promise<WorkflowStepEvent<Payload>>,
206
+ try: () => {
207
+ // SAFETY: Cloudflare returns an event whose payload is determined by the caller's event contract.
208
+ return step.waitForEvent(name, options) as Promise<WorkflowStepEvent<Payload>>;
209
+ },
155
210
  catch: (cause) => new WorkflowStepError({ step: name, operation: "waitForEvent", cause }),
156
211
  }),
157
212
  });
@@ -230,9 +285,7 @@ export const step = <A, E, R>(
230
285
  effect: Effect.Effect<A, E, R>,
231
286
  config?: WorkflowStepConfig,
232
287
  ): Effect.Effect<A, WorkflowStepError, WorkflowStep | Exclude<R, WorkflowStepContext>> =>
233
- Effect.flatMap(WorkflowStep, (workflowStep) =>
234
- workflowStep.do(name, effect, config),
235
- ) as Effect.Effect<A, WorkflowStepError, WorkflowStep | Exclude<R, WorkflowStepContext>>;
288
+ Effect.flatMap(WorkflowStep, (workflowStep) => workflowStep.do(name, effect, config));
236
289
 
237
290
  export const sleep = (
238
291
  name: string,
@@ -3,8 +3,9 @@ import type {
3
3
  Workflow as CloudflareWorkflow,
4
4
  WorkflowInstance as CloudflareWorkflowInstance,
5
5
  WorkflowInstanceCreateOptions as CloudflareWorkflowInstanceCreateOptions,
6
+ WorkflowInstanceRestartOptions as CloudflareWorkflowInstanceRestartOptions,
6
7
  } from "@cloudflare/workers-types";
7
- import { type Context, Data, Effect, Option, Schema as S } from "effect";
8
+ import { type Context, Data, Effect, Option, Predicate, Schema as S } from "effect";
8
9
 
9
10
  import * as Binding from "./Binding";
10
11
  import type * as RpcDefinition from "./RpcDefinition";
@@ -21,13 +22,7 @@ export type WorkflowInstanceCreateBatchOptions<Payload, EncodedPayload = unknown
21
22
  { readonly payload: Payload } & WorkflowInstanceCreateOptions<EncodedPayload>
22
23
  >;
23
24
 
24
- export interface WorkflowInstanceRestartOptions {
25
- readonly from?: {
26
- readonly name?: string;
27
- readonly count?: number;
28
- readonly type?: string;
29
- };
30
- }
25
+ export type WorkflowInstanceRestartOptions = CloudflareWorkflowInstanceRestartOptions;
31
26
 
32
27
  export type WorkflowInstanceStatusName = CloudflareInstanceStatus["status"];
33
28
 
@@ -129,19 +124,22 @@ const tryWorkflowPromise = <A>(
129
124
  catch: (cause) => workflowError(binding, operation, cause),
130
125
  });
131
126
 
132
- export const isWorkflow = <Payload>(value: unknown): value is CloudflareWorkflow<Payload> => {
133
- if (typeof value !== "object" || value === null) {
134
- return false;
135
- }
127
+ type WorkflowBindingValue = S.Schema.Type<typeof S.Unknown>;
136
128
 
137
- const resource = value as Record<string, unknown>;
129
+ const WorkflowBindingSchema = S.declare(
130
+ (value: WorkflowBindingValue): value is CloudflareWorkflow<WorkflowBindingValue> =>
131
+ Predicate.hasProperty(value, "create") &&
132
+ Predicate.isFunction(value.create) &&
133
+ Predicate.hasProperty(value, "createBatch") &&
134
+ Predicate.isFunction(value.createBatch) &&
135
+ Predicate.hasProperty(value, "get") &&
136
+ Predicate.isFunction(value.get),
137
+ );
138
+ const decodeWorkflowBinding = S.decodeUnknownOption(WorkflowBindingSchema);
138
139
 
139
- return (
140
- typeof resource.create === "function" &&
141
- typeof resource.createBatch === "function" &&
142
- typeof resource.get === "function"
143
- );
144
- };
140
+ export const isWorkflow = <Payload>(
141
+ value: WorkflowBindingValue,
142
+ ): value is CloudflareWorkflow<Payload> => Option.isSome(decodeWorkflowBinding(value));
145
143
 
146
144
  export const makeClient = <
147
145
  Payload extends RpcDefinition.ServiceFreeSchema,
@@ -168,12 +166,7 @@ export const makeClient = <
168
166
  pause: operation("pause", () => raw.pause()),
169
167
  resume: operation("resume", () => raw.resume()),
170
168
  terminate: operation("terminate", () => raw.terminate()),
171
- restart: (options) =>
172
- operation("restart", () =>
173
- (raw as { restart(options?: WorkflowInstanceRestartOptions): Promise<void> }).restart(
174
- options,
175
- ),
176
- ),
169
+ restart: (options) => operation("restart", () => raw.restart(options)),
177
170
  status: operation("status", () => raw.status()).pipe(
178
171
  Effect.flatMap((status) =>
179
172
  Effect.gen(function* () {
@@ -187,6 +187,7 @@ export const Tag =
187
187
 
188
188
  const rawUnsafe = Effect.flatMap(tag, (workflow) => workflow.rawUnsafe);
189
189
 
190
+ // SAFETY: Object.assign attaches the definition-derived static helpers declared by TagClass.
190
191
  return Object.assign(tag, {
191
192
  id: workflowDefinition.id,
192
193
  payload: workflowDefinition.payload,
@@ -216,15 +217,16 @@ const wrapHandler = <ROut, const Self extends Definition.Any>(
216
217
  return Effect.fnUntraced(function* (payload: S.Codec.Encoded<Self["payload"]>) {
217
218
  const decodedPayload = yield* decodePayload(payload);
218
219
  const event = yield* WorkflowEntrypoint.WorkflowEvent;
220
+ // SAFETY: replacing the encoded event payload with its schema-decoded value establishes this service type.
219
221
  const decodedEvent = {
220
222
  ...event,
221
223
  payload: decodedPayload,
222
224
  } as WorkflowEntrypoint.WorkflowEventService<S.Schema.Type<Self["payload"]>>;
223
- const result = yield* handler(decodedPayload as S.Schema.Type<Self["payload"]>).pipe(
225
+ const result = yield* handler(decodedPayload).pipe(
224
226
  Effect.provideService(WorkflowEntrypoint.WorkflowEvent, decodedEvent),
225
227
  );
226
228
 
227
- return yield* encodeResult(result as S.Schema.Type<Self["result"]>);
229
+ return yield* encodeResult(result);
228
230
  });
229
231
  };
230
232
 
@@ -1,4 +1,4 @@
1
- import { Effect, Layer } from "effect";
1
+ import { Effect, Layer, Schema } from "effect";
2
2
 
3
3
  import * as RpcDefinition from "../RpcDefinition";
4
4
 
@@ -7,18 +7,36 @@ type EntrypointRpcMethod = (...args: AnyArgs) => Effect.Effect<any, any, any>;
7
7
 
8
8
  export type EntrypointRpc = Record<string, EntrypointRpcMethod>;
9
9
 
10
+ const invokeEntrypointRpcMethod = <Self>(
11
+ self: Self,
12
+ method: EntrypointRpcMethod,
13
+ args: AnyArgs,
14
+ run: (self: Self, effect: Effect.Effect<any, any, any>) => Promise<any>,
15
+ onExit: ((self: Self) => Effect.Effect<void, never, any>) | undefined,
16
+ ): Promise<any> => {
17
+ const handler = Effect.suspend(() => method(...args)).pipe(
18
+ Effect.mapError(RpcDefinition.encodeWireError),
19
+ );
20
+
21
+ return run(
22
+ self,
23
+ onExit === undefined ? handler : handler.pipe(Effect.onExit(() => onExit(self))),
24
+ );
25
+ };
26
+
10
27
  export const provideEntrypointServices = <ROut, LayerError, RIn>(
11
28
  layer: Layer.Layer<ROut, LayerError, RIn>,
12
29
  services: Layer.Layer<RIn, never, never>,
13
30
  ): Layer.Layer<ROut | RIn, LayerError, never> =>
31
+ // SAFETY: provideMerge supplies RIn while retaining it in the output context for entrypoint consumers.
14
32
  layer.pipe(Layer.provideMerge(services)) as Layer.Layer<ROut | RIn, LayerError, never>;
15
33
 
16
- export const defineEntrypointRpcMethods = <Self>(
34
+ export const defineEntrypointRpcMethods = <Self, Prototype extends object = object>(
17
35
  target: string,
18
- prototype: object,
36
+ prototype: Prototype,
19
37
  rpc: EntrypointRpc | undefined,
20
38
  reservedMethodNames: ReadonlySet<string>,
21
- run: (self: Self, effect: Effect.Effect<any, any, any>) => Promise<unknown>,
39
+ run: (self: Self, effect: Effect.Effect<any, any, any>) => Promise<any>,
22
40
  onExit?: (self: Self) => Effect.Effect<void, never, any>,
23
41
  ): void => {
24
42
  const methods = rpc ?? {};
@@ -29,17 +47,15 @@ export const defineEntrypointRpcMethods = <Self>(
29
47
  Object.defineProperty(prototype, key, {
30
48
  enumerable: true,
31
49
  value(this: Self, ...args: AnyArgs) {
32
- const handler = Effect.suspend(() => method(...args)).pipe(
33
- Effect.mapError(RpcDefinition.encodeWireError),
34
- );
35
-
36
- return run(
37
- this,
38
- onExit === undefined ? handler : handler.pipe(Effect.onExit(() => onExit(this))),
39
- );
50
+ return invokeEntrypointRpcMethod(this, method, args, run, onExit);
40
51
  },
41
52
  });
42
53
  }
43
54
  };
44
55
 
45
- export const assumeEntrypointClass = <Class>(entrypoint: unknown): Class => entrypoint as Class;
56
+ type EntrypointClassValue = Schema.Schema.Type<typeof Schema.Unknown>;
57
+
58
+ export const assumeEntrypointClass = <Class>(entrypoint: EntrypointClassValue): Class => {
59
+ // SAFETY: callers pass a freshly declared Cloudflare entrypoint subclass matching Class's constructor contract.
60
+ return entrypoint as Class;
61
+ };
@@ -3,12 +3,12 @@
3
3
  * for use in tagged error `message` getters.
4
4
  */
5
5
  export const causeMessage = (cause: unknown): string => {
6
- if (typeof cause === "object" && cause !== null) {
7
- const message = Reflect.get(cause, "message");
8
-
9
- if (typeof message === "string" && message.length > 0) {
10
- return message;
11
- }
6
+ if (
7
+ Predicate.hasProperty(cause, "message") &&
8
+ Predicate.isString(cause.message) &&
9
+ cause.message.length > 0
10
+ ) {
11
+ return cause.message;
12
12
  }
13
13
 
14
14
  try {
@@ -22,3 +22,4 @@ export const causeMessage = (cause: unknown): string => {
22
22
  export const violationsMessage = (
23
23
  violations: ReadonlyArray<{ readonly path: string; readonly message: string }>,
24
24
  ): string => violations.map((violation) => `${violation.path}: ${violation.message}`).join("; ");
25
+ import { Predicate } from "effect";
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Predicate, Schema } from "effect";
2
2
 
3
3
  import type * as CloudflareRpc from "../Rpc";
4
4
 
@@ -6,7 +6,7 @@ type AnyArgs = Array<any>;
6
6
 
7
7
  export type AsyncMethodKey<Api> = {
8
8
  [Key in keyof Api]-?: Key extends string
9
- ? Api[Key] extends (...args: AnyArgs) => Promise<unknown>
9
+ ? Api[Key] extends (...args: AnyArgs) => Promise<any>
10
10
  ? Key
11
11
  : never
12
12
  : never;
@@ -14,7 +14,7 @@ export type AsyncMethodKey<Api> = {
14
14
 
15
15
  export type AsyncMethodArgs<Api, Method extends keyof Api> = Api[Method] extends (
16
16
  ...args: infer Args
17
- ) => Promise<unknown>
17
+ ) => Promise<any>
18
18
  ? Args
19
19
  : never;
20
20
 
@@ -28,11 +28,24 @@ export type AsyncMethodCloudflareReturn<Api, Method extends keyof Api> = Cloudfl
28
28
  AsyncMethodSuccess<Api, Method>
29
29
  >;
30
30
 
31
- const isPropertyTarget = (value: unknown): value is object =>
32
- (typeof value === "object" || typeof value === "function") && value !== null;
31
+ type RpcTargetValue = Schema.Schema.Type<typeof Schema.Unknown>;
32
+ type RpcMethodOwner<Method extends PropertyKey> = {
33
+ readonly [Key in Method]?: RpcTargetValue;
34
+ };
35
+
36
+ const invokeWithReceiver = <Method extends PropertyKey, Args extends AnyArgs, Return>(
37
+ value: (...args: Args) => Return,
38
+ receiver: RpcMethodOwner<Method>,
39
+ args: Args,
40
+ ): Return => {
41
+ const result: RpcTargetValue = Function.prototype.apply.call(value, receiver, args);
42
+
43
+ // SAFETY: the native apply intrinsic returns value's declared Return while avoiding proxy property reads.
44
+ return result as Return;
45
+ };
33
46
 
34
47
  export const lookupRpcMethod = <Api, Method extends AsyncMethodKey<Api>, Error>(
35
- target: unknown,
48
+ target: RpcTargetValue,
36
49
  method: Method,
37
50
  makeError: (cause: unknown) => Error,
38
51
  ): Effect.Effect<
@@ -41,19 +54,29 @@ export const lookupRpcMethod = <Api, Method extends AsyncMethodKey<Api>, Error>(
41
54
  > =>
42
55
  Effect.try({
43
56
  try: () => {
44
- if (!isPropertyTarget(target)) {
57
+ if (!Predicate.isObjectKeyword(target)) {
45
58
  throw new TypeError(`RPC target is not object-like`);
46
59
  }
47
60
 
48
- const value = Reflect.get(target, method);
61
+ // SAFETY: this assertion enables one proxy property read; the result is validated before invocation.
62
+ const receiver = target as RpcMethodOwner<Method>;
63
+ const value = receiver[method];
49
64
 
50
- if (typeof value !== "function") {
65
+ if (!Predicate.isFunction(value)) {
51
66
  throw new TypeError(`RPC method "${String(method)}" is not callable`);
52
67
  }
53
68
 
54
- return ((...args: AsyncMethodArgs<Api, Method>) => Reflect.apply(value, target, args)) as (
69
+ // SAFETY: the runtime check proves callability; Api and Method supply this binding's call contract.
70
+ const callable = value as (
55
71
  ...args: AsyncMethodArgs<Api, Method>
56
72
  ) => AsyncMethodCloudflareReturn<Api, Method>;
73
+
74
+ return (...args: AsyncMethodArgs<Api, Method>): AsyncMethodCloudflareReturn<Api, Method> =>
75
+ invokeWithReceiver<
76
+ Method,
77
+ AsyncMethodArgs<Api, Method>,
78
+ AsyncMethodCloudflareReturn<Api, Method>
79
+ >(callable, receiver, args);
57
80
  },
58
81
  catch: makeError,
59
82
  });
@@ -63,7 +86,7 @@ export const invokeRpcMethod = Effect.fnUntraced(function* <
63
86
  Method extends AsyncMethodKey<Api>,
64
87
  Error,
65
88
  >(
66
- target: unknown,
89
+ target: RpcTargetValue,
67
90
  method: Method,
68
91
  args: AsyncMethodArgs<Api, Method>,
69
92
  makeError: (cause: unknown) => Error,
@@ -26,27 +26,38 @@ export const makeEntrypointRuntime = <ROut, LayerError, Services>(
26
26
  return ManagedRuntime.make(provideEntrypointServices(layer, entrypointServices));
27
27
  };
28
28
 
29
- /**
30
- * Runs a Cloudflare event effect on an entrypoint runtime.
31
- *
32
- * When an event layer is given it is built inside the event's Effect scope
33
- * (finalized when the event effect completes). The casts widen away the
34
- * `REvent` requirement satisfied by the event layer; TypeScript cannot reduce
35
- * `Exclude<...>` over the generic union.
36
- */
37
- export const runEventPromise = <A, E, R, REvent, EventLayerError, LayerError>(
29
+ /** Runs an event effect whose requirements are already available in the runtime. */
30
+ export function runEventPromise<A, E, R, LayerError>(
31
+ runtime: ManagedRuntime.ManagedRuntime<R, LayerError>,
32
+ effect: Effect.Effect<A, E, NoInfer<R> | Scope.Scope>,
33
+ eventLayer?: undefined,
34
+ ): Promise<A>;
35
+ /** Builds and provides an event layer inside the event effect's scope. */
36
+ export function runEventPromise<A, E, R, REvent, EventLayerError, LayerError>(
38
37
  runtime: ManagedRuntime.ManagedRuntime<R, LayerError>,
39
- effect: Effect.Effect<A, E, R | REvent | Scope.Scope>,
40
- eventLayer: Layer.Layer<REvent, EventLayerError, R> | undefined,
41
- ): Promise<A> => {
42
- const withEventLayer =
43
- eventLayer === undefined
44
- ? (effect as Effect.Effect<A, E, R | Scope.Scope>)
45
- : (effect.pipe(Effect.provide(eventLayer, { local: true })) as Effect.Effect<
46
- A,
47
- E | EventLayerError,
48
- R | Scope.Scope
49
- >);
38
+ effect: Effect.Effect<A, E, NoInfer<R> | REvent | Scope.Scope>,
39
+ eventLayer: Layer.Layer<REvent, EventLayerError, NoInfer<R>>,
40
+ ): Promise<A>;
41
+ export function runEventPromise<A, E, R, REvent, EventLayerError, LayerError>(
42
+ ...args:
43
+ | readonly [
44
+ runtime: ManagedRuntime.ManagedRuntime<R, LayerError>,
45
+ effect: Effect.Effect<A, E, R | Scope.Scope>,
46
+ eventLayer?: undefined,
47
+ ]
48
+ | readonly [
49
+ runtime: ManagedRuntime.ManagedRuntime<R, LayerError>,
50
+ effect: Effect.Effect<A, E, R | REvent | Scope.Scope>,
51
+ eventLayer: Layer.Layer<REvent, EventLayerError, R>,
52
+ ]
53
+ ): Promise<A> {
54
+ const [runtime, effect, eventLayer] = args;
50
55
 
51
- return runtime.runPromise(Effect.scoped(withEventLayer));
52
- };
56
+ if (eventLayer === undefined) {
57
+ return runtime.runPromise(Effect.scoped(effect));
58
+ }
59
+
60
+ return runtime.runPromise(
61
+ Effect.scoped(effect.pipe(Effect.provide(eventLayer, { local: true }))),
62
+ );
63
+ }
@@ -1,14 +1,15 @@
1
1
  import { Effect, Option } from "effect";
2
2
  import { OtlpExporter } from "effect/unstable/observability";
3
3
 
4
- const logFlushFailure = Effect.logError("Telemetry flush failed").pipe(Effect.ignoreCause);
5
-
6
- const logFlushSchedulingFailure = Effect.logError("Telemetry flush scheduling failed").pipe(
7
- Effect.ignoreCause,
8
- );
4
+ /** Maximum event lifetime spent on an internally scheduled telemetry flush. */
5
+ const scheduledFlushTimeout = "2 seconds";
9
6
 
10
7
  /**
11
8
  * Schedules a configured OTLP flusher through the current Cloudflare event.
9
+ *
10
+ * The handed-off effect is best-effort: it settles within two seconds and
11
+ * silently absorbs every failure cause so telemetry cannot extend or fail the
12
+ * user event, or recursively log through an unhealthy exporter.
12
13
  */
13
14
  export const scheduleTelemetryFlush = <R>(
14
15
  waitUntil: (flush: Effect.Effect<void>) => Effect.Effect<void, never, R>,
@@ -20,8 +21,14 @@ export const scheduleTelemetryFlush = <R>(
20
21
  return;
21
22
  }
22
23
 
23
- // Exporters and platform schedulers are foreign boundaries. Preserve a
24
- // useful diagnostic without retaining their arbitrary failure causes in
25
- // logs, where payloads, headers, or credentials could otherwise escape.
26
- yield* waitUntil(flusher.value.flush.pipe(Effect.catchCause(() => logFlushFailure)));
27
- }).pipe(Effect.catchCause(() => logFlushSchedulingFailure));
24
+ // Exporters and platform schedulers are foreign boundaries. Do not log
25
+ // their arbitrary failure causes: the configured logger may be the same
26
+ // unhealthy OTLP exporter and recursively increase its backlog.
27
+ yield* waitUntil(
28
+ flusher.value.flush.pipe(
29
+ Effect.timeoutOption(scheduledFlushTimeout),
30
+ Effect.ignoreCause,
31
+ Effect.asVoid,
32
+ ),
33
+ );
34
+ }).pipe(Effect.ignoreCause);