effect-cf 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +2 -2
  3. package/dist/{Hyperdrive-RTjgirMV.d.mts → Hyperdrive-COyg7R26.d.mts} +40 -13
  4. package/dist/HyperdrivePg.d.mts +13 -5
  5. package/dist/HyperdrivePg.mjs +12 -7
  6. package/dist/index.d.mts +708 -549
  7. package/dist/index.mjs +946 -589
  8. package/package.json +10 -5
  9. package/src/AiGateway.ts +82 -47
  10. package/src/AnalyticsEngine.ts +74 -58
  11. package/src/Binding.ts +66 -13
  12. package/src/BrowserRendering.ts +30 -7
  13. package/src/Cache.ts +32 -8
  14. package/src/CloudflareOtlp.ts +32 -9
  15. package/src/ContainerNamespace.ts +87 -55
  16. package/src/D1.ts +13 -3
  17. package/src/DurableObject.ts +27 -164
  18. package/src/DurableObjectAlarm.ts +30 -9
  19. package/src/DurableObjectDefinition.ts +58 -54
  20. package/src/DurableObjectNamespace.ts +99 -96
  21. package/src/DurableObjectRpcWebSocket.ts +14 -6
  22. package/src/DurableObjectSqlite.ts +6 -3
  23. package/src/DurableObjectState.ts +6 -9
  24. package/src/DurableObjectStorage.ts +7 -1
  25. package/src/DurableObjectWebSocket.ts +51 -41
  26. package/src/Email.ts +40 -13
  27. package/src/Environment.ts +26 -6
  28. package/src/Hyperdrive.ts +2 -2
  29. package/src/HyperdrivePg.ts +25 -13
  30. package/src/Images.ts +77 -82
  31. package/src/Kv.ts +37 -27
  32. package/src/Queue.ts +6 -1
  33. package/src/QueueBinding.ts +18 -21
  34. package/src/QueueDefinition.ts +15 -14
  35. package/src/R2.ts +107 -29
  36. package/src/Rpc.ts +3 -1
  37. package/src/RpcDefinition.ts +177 -133
  38. package/src/ServiceBinding.ts +112 -108
  39. package/src/Vectorize.ts +59 -14
  40. package/src/Worker.ts +205 -198
  41. package/src/WorkerDefinition.ts +5 -10
  42. package/src/WorkersAi.ts +27 -17
  43. package/src/Workflow.ts +54 -49
  44. package/src/WorkflowBinding.ts +19 -11
  45. package/src/WorkflowDefinition.ts +24 -27
  46. package/src/index.ts +4 -3
  47. package/src/internal/Entrypoint.ts +3 -1
  48. package/src/internal/ErrorMessage.ts +24 -0
  49. package/src/internal/RpcInvocation.ts +11 -8
  50. package/src/internal/Runtime.ts +52 -0
  51. package/src/internal/WorkerContext.ts +14 -2
  52. package/src/Entry.ts +0 -259
  53. package/src/internal/Clock.ts +0 -37
package/src/Worker.ts CHANGED
@@ -1,37 +1,48 @@
1
1
  import { WorkerEntrypoint as CloudflareWorkerEntrypoint } from "cloudflare:workers";
2
2
  import {
3
3
  type Cause,
4
- ConfigProvider,
5
4
  Context,
6
5
  Effect,
7
6
  Layer,
8
- ManagedRuntime,
9
- type Schema as S,
7
+ type ManagedRuntime,
8
+ Option,
10
9
  type Scope,
11
10
  } from "effect";
12
- import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
11
+ import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
12
+ import { OtlpExporter } from "effect/unstable/observability";
13
13
 
14
- import type * as Binding from "./Binding";
15
- import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
14
+ import { WorkerEnvironment, type WorkerEnv } from "./Environment";
16
15
  import { fromMessage, fromMessageBatch, type QueueHandler } from "./Queue";
17
- import type * as Rpc from "./Rpc";
18
16
  import type * as RpcDefinition from "./RpcDefinition";
19
- import type * as ServiceBinding from "./ServiceBinding";
20
- import * as WorkerDefinition from "./WorkerDefinition";
21
- import * as CloudflareClock from "./internal/Clock";
22
17
  import * as Entrypoint from "./internal/Entrypoint";
23
- import { fromExecutionContext, type RunWaitUntilEffect } from "./internal/WorkerContext";
18
+ import * as Runtime from "./internal/Runtime";
19
+ import { fromExecutionContext } from "./internal/WorkerContext";
24
20
 
21
+ /**
22
+ * Access to Cloudflare's native `ExecutionContext`.
23
+ */
25
24
  export class ExecutionContext extends Context.Service<
26
25
  ExecutionContext,
27
26
  globalThis.ExecutionContext
28
27
  >()("effect-cf/ExecutionContext") {}
29
28
 
29
+ /**
30
+ * Options for {@link WorkerContextService.waitUntil}.
31
+ */
30
32
  export interface WorkerContextWaitUntilOptions<E, R> {
33
+ /**
34
+ * Failure mode for the background effect: `"observe"` (default) logs or
35
+ * routes the failure to `onFailure`, while `"propagate"` also rejects the
36
+ * native `waitUntil` promise.
37
+ */
31
38
  readonly mode?: "observe" | "propagate";
39
+ /** Custom failure handler for the background effect. */
32
40
  readonly onFailure?: (cause: Cause.Cause<E>) => Effect.Effect<void, never, R>;
33
41
  }
34
42
 
43
+ /**
44
+ * Effect wrapper around `ExecutionContext` background APIs.
45
+ */
35
46
  export interface WorkerContextService {
36
47
  readonly raw: globalThis.ExecutionContext;
37
48
  waitUntil<A, E, R, R2 = never>(
@@ -45,14 +56,32 @@ export interface WorkerContextService {
45
56
  readonly passThroughOnException: Effect.Effect<void>;
46
57
  }
47
58
 
59
+ /**
60
+ * Service used inside handlers to schedule background work via `waitUntil`.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const handler = Effect.gen(function* () {
65
+ * const ctx = yield* Worker.WorkerContext;
66
+ * yield* ctx.waitUntil(Effect.log("flush analytics"));
67
+ * return new Response("ok");
68
+ * });
69
+ * ```
70
+ */
48
71
  export class WorkerContext extends Context.Service<WorkerContext, WorkerContextService>()(
49
72
  "effect-cf/WorkerContext",
50
73
  ) {}
51
74
 
75
+ /**
76
+ * Access to the incoming `Request` currently handled by a worker or Durable Object fetch.
77
+ */
52
78
  export class NativeRequest extends Context.Service<NativeRequest, Request>()(
53
79
  "effect-cf/NativeRequest",
54
80
  ) {}
55
81
 
82
+ /**
83
+ * Returns `true` when the request is a websocket upgrade request.
84
+ */
56
85
  export const isWebSocketUpgrade = (request: Request): boolean =>
57
86
  request.headers.get("Upgrade")?.toLowerCase() === "websocket";
58
87
 
@@ -102,17 +131,32 @@ type RunOptions = {
102
131
  };
103
132
 
104
133
  const RunSymbol = Symbol.for("effect-cf/Worker/run");
134
+ const FetchSymbol = Symbol.for("effect-cf/Worker/fetch");
105
135
 
136
+ /**
137
+ * Successful result of a worker fetch handler: either a native `Response`
138
+ * (returned to Cloudflare untouched) or an Effect `HttpServerResponse`
139
+ * (rendered through the Effect HTTP pipeline).
140
+ */
106
141
  export type WorkerFetchSuccess = Response | HttpServerResponse.HttpServerResponse;
107
142
 
143
+ /**
144
+ * Effect type for `fetch` handlers executed by {@link make}.
145
+ */
108
146
  export type WorkerHandler<ROut, A = WorkerFetchSuccess> = Effect.Effect<
109
147
  A,
110
148
  unknown,
111
149
  WorkerFetchContext<ROut>
112
150
  >;
113
151
 
152
+ /**
153
+ * Effect type for worker RPC handlers.
154
+ */
114
155
  export type WorkerRpcHandler<ROut, A = unknown> = Effect.Effect<A, unknown, WorkerRpcContext<ROut>>;
115
156
 
157
+ /**
158
+ * Shape of worker RPC handlers passed to {@link make}.
159
+ */
116
160
  export type WorkerRpc<ROut> = Record<string, (...args: Array<any>) => WorkerRpcHandler<ROut>>;
117
161
 
118
162
  export type WorkerRpcShape<Rpc extends WorkerRpc<ROut>, ROut> = {
@@ -139,6 +183,9 @@ export type RpcHandlers<ROut, Api> = {
139
183
  : never;
140
184
  };
141
185
 
186
+ /**
187
+ * Options for creating a worker class with Effect handlers.
188
+ */
142
189
  export interface WorkerOptions<
143
190
  RRuntime,
144
191
  REvent = never,
@@ -153,12 +200,15 @@ export interface WorkerOptions<
153
200
  * trace/log exporters that should flush at event completion.
154
201
  */
155
202
  readonly eventLayer?: Layer.Layer<REvent, EventLayerError, WorkerBaseContext<RRuntime>>;
203
+ /** Main request handler. */
156
204
  readonly fetch?: Effect.Effect<
157
205
  WorkerFetchSuccess,
158
206
  unknown,
159
207
  WorkerFetchContext<RRuntime | REvent>
160
208
  >;
209
+ /** Queue consumer handler invoked per message batch. */
161
210
  readonly queue?: QueueHandler<RRuntime | REvent>;
211
+ /** Optional RPC methods exposed as class instance methods. */
162
212
  readonly rpc?: Rpc;
163
213
  }
164
214
 
@@ -169,6 +219,9 @@ export type FetchWorkerOptions<RRuntime, REvent = never, EventLayerError = never
169
219
  readonly rpc?: never;
170
220
  };
171
221
 
222
+ /**
223
+ * Cloudflare `WorkerEntrypoint` constructor produced by {@link make}.
224
+ */
172
225
  export type WorkerClass<Rpc extends WorkerRpc<ROut>, ROut> = new (
173
226
  ctx: globalThis.ExecutionContext,
174
227
  env: WorkerEnv,
@@ -177,6 +230,9 @@ export type WorkerClass<Rpc extends WorkerRpc<ROut>, ROut> = new (
177
230
  queue(batch: globalThis.MessageBatch): Promise<void>;
178
231
  } & WorkerRpcShape<Rpc, ROut>;
179
232
 
233
+ /**
234
+ * `ExportedHandler`-compatible fetch object produced by {@link makeFetchHandler}.
235
+ */
180
236
  export interface FetchHandler<Env extends WorkerEnv = WorkerEnv> {
181
237
  readonly fetch: (
182
238
  request: Request,
@@ -185,33 +241,81 @@ export interface FetchHandler<Env extends WorkerEnv = WorkerEnv> {
185
241
  ) => Promise<Response>;
186
242
  }
187
243
 
188
- export const renderHttpResponse = <A extends HttpServerResponse.HttpServerResponse, E, R>(
189
- effect: Effect.Effect<A, E, R>,
190
- ): Effect.Effect<Response, E, R> =>
191
- Effect.flatMap(effect, (response) =>
192
- Effect.map(Effect.context<never>(), (context) =>
193
- HttpServerResponse.toWeb(response, { context }),
194
- ),
195
- );
196
-
197
244
  const renderFetchSuccess = <E, R>(
198
245
  effect: Effect.Effect<WorkerFetchSuccess, E, R>,
199
- ): Effect.Effect<Response, E, R> =>
200
- Effect.flatMap(effect, (response) =>
201
- response instanceof Response
202
- ? Effect.succeed(response)
203
- : Effect.map(Effect.context<never>(), (context) =>
204
- HttpServerResponse.toWeb(response, { context }),
205
- ),
206
- );
246
+ ): Effect.Effect<Response, E, Exclude<R, Scope.Scope> | HttpServerRequest.HttpServerRequest> =>
247
+ Effect.suspend(() => {
248
+ // Native `Response` values bypass all HTTP response processing: WebSocket
249
+ // upgrade responses and app-level `HttpEffect.toHandled` results pass
250
+ // through untouched. Pre-response handlers still run against a placeholder
251
+ // response, but their output is discarded for native `Response` results.
252
+ let nativeResponse: Response | undefined;
253
+ let webResponse: Response | undefined;
254
+
255
+ const handled = HttpEffect.toHandled(
256
+ Effect.flatMap(effect, (response) => {
257
+ if (response instanceof Response) {
258
+ nativeResponse = response;
259
+
260
+ return Effect.succeed(HttpServerResponse.empty());
261
+ }
262
+
263
+ return Effect.succeed(response);
264
+ }),
265
+ (request, response) =>
266
+ nativeResponse !== undefined
267
+ ? Effect.void
268
+ : Effect.map(Effect.context<never>(), (context) => {
269
+ webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
270
+ withoutBody: request.method === "HEAD",
271
+ context,
272
+ });
273
+ }),
274
+ );
275
+
276
+ // `toHandled` re-fails with the original cause after rendering the error
277
+ // response, so both branches return the response captured above.
278
+ const captured = () => nativeResponse ?? webResponse ?? new Response(null, { status: 500 });
279
+
280
+ return Effect.matchCause(handled, { onFailure: captured, onSuccess: captured });
281
+ });
207
282
 
208
283
  const isWorkerOptions = <ROut, REvent, EventLayerError, Rpc extends WorkerRpc<ROut | REvent>>(
209
284
  options: WorkerOptions<ROut, REvent, EventLayerError, Rpc> | WorkerHandler<ROut>,
210
- ): options is WorkerOptions<ROut, REvent, EventLayerError, Rpc> =>
211
- typeof options === "object" &&
212
- options !== null &&
213
- ("eventLayer" in options || "fetch" in options || "queue" in options || "rpc" in options);
214
-
285
+ ): options is WorkerOptions<ROut, REvent, EventLayerError, Rpc> => !Effect.isEffect(options);
286
+
287
+ /**
288
+ * Drains buffered OTLP telemetry once the fetch handler has produced its
289
+ * response.
290
+ *
291
+ * The flush is handed to `ctx.waitUntil` so it never delays the response
292
+ * (including streaming bodies still being consumed). It is a no-op when the
293
+ * context does not contain an `OtlpExporter.Flusher`.
294
+ */
295
+ const scheduleTelemetryFlush: Effect.Effect<void, never, WorkerContext> = Effect.flatMap(
296
+ Effect.serviceOption(OtlpExporter.Flusher),
297
+ Option.match({
298
+ onNone: () => Effect.void,
299
+ onSome: (flusher) =>
300
+ Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flusher.flush)),
301
+ }),
302
+ );
303
+
304
+ /**
305
+ * Creates a Cloudflare worker class backed by a single managed Effect runtime.
306
+ *
307
+ * The runtime layer is built when Cloudflare constructs the class and reused
308
+ * for every event the instance handles. Layer finalizers do not run when the
309
+ * isolate is evicted; Cloudflare provides no shutdown hook. Use `eventLayer`
310
+ * for resources that must be finalized per event.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * export default Worker.make(Layer.empty, {
315
+ * fetch: Effect.succeed(new Response("ok")),
316
+ * });
317
+ * ```
318
+ */
215
319
  export function make<ROut, LayerError>(
216
320
  layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
217
321
  fetch: WorkerHandler<ROut>,
@@ -246,69 +350,58 @@ export function make<
246
350
  constructor(ctx: globalThis.ExecutionContext, env: WorkerEnv) {
247
351
  super(ctx, env);
248
352
 
249
- let runWaitUntilEffect: RunWaitUntilEffect = () =>
250
- Promise.reject(new Error("WorkerContext runtime is not initialized"));
251
-
252
- const services = Layer.mergeAll(
253
- CloudflareClock.layer,
254
- Layer.succeed(ExecutionContext, ctx),
255
- ConfigProvider.layer(Effect.succeed(WorkerConfig.providerFromEnv(env))),
256
- Layer.succeed(
257
- WorkerContext,
258
- fromExecutionContext(ctx, (effect) => runWaitUntilEffect(effect)),
259
- ),
260
- Layer.succeed(WorkerEnvironment, env),
261
- ) as Layer.Layer<ExecutionContext | WorkerContext | WorkerEnvironment, never, never>;
262
-
263
- const runtimeLayer = Entrypoint.provideEntrypointServices<
353
+ this.runtime = Runtime.makeEntrypointRuntime<
264
354
  ROut,
265
355
  LayerError,
266
- ExecutionContext | WorkerContext | WorkerEnvironment
267
- >(layer, services);
268
-
269
- this.runtime = ManagedRuntime.make(runtimeLayer);
270
- runWaitUntilEffect = <A, E>(effect: Effect.Effect<A, E, never>) =>
271
- this.runtime.runPromiseExit(effect as Effect.Effect<A, E, RuntimeContext<ROut>>);
356
+ ExecutionContext | WorkerContext
357
+ >(
358
+ layer,
359
+ env,
360
+ Layer.mergeAll(
361
+ Layer.succeed(ExecutionContext, ctx),
362
+ Layer.succeed(WorkerContext, fromExecutionContext(ctx)),
363
+ ),
364
+ );
272
365
  }
273
366
 
274
367
  [RunSymbol]<A, E>(
275
368
  effect: Effect.Effect<A, E, RuntimeContext<ROut> | REvent | Scope.Scope>,
276
369
  runOptions: RunOptions = {},
277
370
  ): Promise<A> {
278
- const effectWithEventLayer =
279
- runOptions.eventLayer === false || options.eventLayer === undefined
280
- ? effect
281
- : effect.pipe(Effect.provide(options.eventLayer, { local: true }));
282
-
283
- return this.runtime.runPromise(
284
- Effect.scoped(
285
- effectWithEventLayer as Effect.Effect<
286
- A,
287
- E | EventLayerError,
288
- RuntimeContext<ROut> | Scope.Scope
289
- >,
290
- ),
371
+ return Runtime.runEventPromise(
372
+ this.runtime,
373
+ effect,
374
+ runOptions.eventLayer === false ? undefined : options.eventLayer,
291
375
  );
292
376
  }
293
377
 
294
378
  fetch(request: Request): Promise<Response> {
379
+ return this[FetchSymbol](request);
380
+ }
381
+
382
+ [FetchSymbol](
383
+ request: Request,
384
+ requestContext?: globalThis.ExecutionContext,
385
+ ): Promise<Response> {
295
386
  const fetchHandler = options.fetch;
296
387
 
297
388
  if (fetchHandler === undefined) {
298
389
  return Promise.resolve(new Response("Not Found", { status: 404 }));
299
390
  }
300
391
 
392
+ const ctx = requestContext ?? this.ctx;
301
393
  const requestServices = Layer.mergeAll(
302
394
  Layer.succeed(NativeRequest, request),
303
395
  Layer.succeed(HttpServerRequest.HttpServerRequest, HttpServerRequest.fromWeb(request)),
396
+ Layer.succeed(ExecutionContext, ctx),
397
+ Layer.succeed(WorkerContext, fromExecutionContext(ctx)),
304
398
  );
305
399
 
306
400
  return this[RunSymbol](
307
- renderFetchSuccess(fetchHandler).pipe(Effect.provide(requestServices)) as Effect.Effect<
308
- Response,
309
- unknown,
310
- RuntimeContext<ROut> | REvent | Scope.Scope
311
- >,
401
+ renderFetchSuccess(fetchHandler).pipe(
402
+ Effect.tap(() => scheduleTelemetryFlush),
403
+ Effect.provide(requestServices),
404
+ ) as Effect.Effect<Response, unknown, RuntimeContext<ROut> | REvent | Scope.Scope>,
312
405
  );
313
406
  }
314
407
 
@@ -336,6 +429,21 @@ export function make<
336
429
  return Entrypoint.assumeEntrypointClass<WorkerClass<Rpc, ROut | REvent>>(EffectWorker);
337
430
  }
338
431
 
432
+ interface FetchCapableInstance {
433
+ [FetchSymbol](request: Request, requestContext: globalThis.ExecutionContext): Promise<Response>;
434
+ }
435
+
436
+ /**
437
+ * Creates an `ExportedHandler`-compatible `{ fetch }` object backed by
438
+ * {@link make}.
439
+ *
440
+ * The Effect runtime is cached per `env` identity, so the user layer builds
441
+ * once per isolate instead of once per request. `ExecutionContext`-backed
442
+ * services (`Worker.ExecutionContext`, `Worker.WorkerContext`) are still
443
+ * provided fresh for every request. Layer finalizers do not run when the
444
+ * isolate is evicted; Cloudflare provides no shutdown hook. Use `eventLayer`
445
+ * for resources that must be finalized per event.
446
+ */
339
447
  export const makeFetchHandler = <
340
448
  ROut,
341
449
  LayerError,
@@ -347,136 +455,35 @@ export const makeFetchHandler = <
347
455
  options: FetchWorkerOptions<ROut, REvent, EventLayerError>,
348
456
  ): FetchHandler<Env> => {
349
457
  const WorkerClass = make(layer, options);
458
+ const instances = new WeakMap<WorkerEnv, FetchCapableInstance>();
350
459
 
351
460
  return {
352
- fetch: (request, env, ctx) => Promise.resolve(new WorkerClass(ctx, env).fetch(request)),
353
- };
354
- };
355
-
356
- export type ServiceFreeSchema = S.Codec<any, any, never, never>;
357
-
358
- export interface Method<
359
- Args extends ReadonlyArray<ServiceFreeSchema> = ReadonlyArray<ServiceFreeSchema>,
360
- Success extends ServiceFreeSchema = ServiceFreeSchema,
361
- > {
362
- readonly args: Args;
363
- readonly success: Success;
364
- }
461
+ fetch: (request, env, ctx) => {
462
+ let instance = instances.get(env);
365
463
 
366
- export namespace Method {
367
- export type Any = Method<ReadonlyArray<ServiceFreeSchema>, ServiceFreeSchema>;
368
-
369
- type ArgsFromSchemas<Args extends ReadonlyArray<ServiceFreeSchema>> = Args extends readonly []
370
- ? []
371
- : Args extends readonly [
372
- infer Head extends ServiceFreeSchema,
373
- ...infer Tail extends ReadonlyArray<ServiceFreeSchema>,
374
- ]
375
- ? [S.Schema.Type<Head>, ...ArgsFromSchemas<Tail>]
376
- : Array<S.Schema.Type<Args[number]>>;
377
-
378
- export type Args<Self extends Any> = ArgsFromSchemas<Self["args"]>;
379
-
380
- export type Success<Self extends Any> = S.Schema.Type<Self["success"]>;
381
- }
382
-
383
- export type Methods = Record<string, Method.Any>;
384
-
385
- export type NoReservedMethods<MethodsShape extends Methods> =
386
- Extract<keyof MethodsShape, ReservedMethodName> extends never ? MethodsShape : never;
387
-
388
- export interface Definition<Id extends string = string, MethodsShape extends Methods = Methods> {
389
- readonly id: Id;
390
- readonly methods: MethodsShape;
391
- }
392
-
393
- export namespace Definition {
394
- export type Any = Definition<string, Methods>;
395
- }
396
-
397
- export type ServerApi<Self extends Definition.Any> = {
398
- readonly [Key in keyof Self["methods"]]: (
399
- ...args: Method.Args<Self["methods"][Key]>
400
- ) => Promise<Method.Success<Self["methods"][Key]>>;
401
- };
402
-
403
- export type Api<Self extends Definition.Any> = Rpc.Provider<ServerApi<Self>, ReservedMethodName>;
404
-
405
- export type Handlers<ROut, Self extends Definition.Any> = {
406
- readonly [Key in keyof Self["methods"]]: (
407
- ...args: Method.Args<Self["methods"][Key]>
408
- ) => WorkerRpcHandler<ROut, Method.Success<Self["methods"][Key]>>;
409
- };
410
-
411
- export interface Options<
412
- ROut,
413
- Self extends Definition.Any,
414
- REvent = never,
415
- EventLayerError = never,
416
- > extends Omit<WorkerOptions<ROut, REvent, EventLayerError, Handlers<ROut | REvent, Self>>, "rpc"> {
417
- readonly rpc: Handlers<ROut | REvent, Self>;
418
- }
464
+ if (instance === undefined) {
465
+ instance = new WorkerClass(ctx, env) as unknown as FetchCapableInstance;
466
+ instances.set(env, instance);
467
+ }
419
468
 
420
- export type LayerOptions = WorkerDefinition.LayerOptions;
421
-
422
- export type TagClass<Self, Id extends string, MethodsShape extends Methods> = Context.ServiceClass<
423
- Self,
424
- Id,
425
- ServiceBinding.ServiceBindingEffectClient<
426
- Api<Definition<Id, MethodsShape>>,
427
- Definition<Id, MethodsShape>
428
- >
429
- > &
430
- ServiceBinding.ServiceBindingStaticClient<
431
- Self,
432
- Api<Definition<Id, MethodsShape>>,
433
- Definition<Id, MethodsShape>
434
- > & {
435
- readonly id: Id;
436
- readonly methods: MethodsShape;
437
- readonly make: <ROut, LayerError, REvent = never, EventLayerError = never>(
438
- layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
439
- options: Options<ROut, Definition<Id, MethodsShape>, REvent, EventLayerError>,
440
- ) => WorkerClass<Handlers<ROut | REvent, Definition<Id, MethodsShape>>, ROut | REvent>;
441
- readonly layer: (
442
- options: LayerOptions,
443
- ) => Layer.Layer<
444
- Self,
445
- Binding.BindingNotFoundError | Binding.BindingValidationError,
446
- WorkerEnvironment
447
- >;
469
+ return instance[FetchSymbol](request, ctx);
470
+ },
448
471
  };
449
-
450
- export type TagFactory = <Self>() => <Id extends string, const MethodsShape extends Methods>(
451
- id: Id,
452
- methods: MethodsShape & NoReservedMethods<MethodsShape>,
453
- ) => TagClass<Self, Id, MethodsShape>;
454
-
455
- export const Tag = WorkerDefinition.Tag as unknown as TagFactory;
456
-
457
- export const method = WorkerDefinition.method as {
458
- <Success extends ServiceFreeSchema>(definition: {
459
- readonly success: Success;
460
- }): Method<readonly [], Success>;
461
- <
462
- const Args extends ReadonlyArray<ServiceFreeSchema>,
463
- Success extends ServiceFreeSchema,
464
- >(definition: {
465
- readonly args: Args;
466
- readonly success: Success;
467
- }): Method<Args, Success>;
468
472
  };
469
473
 
470
- export const implement = WorkerDefinition.implement as unknown as <
471
- ROut,
472
- const Self extends Definition.Any,
473
- >(
474
- _definition: Self,
475
- handlers: Handlers<ROut, Self>,
476
- ) => Handlers<ROut, Self>;
477
-
478
- export type HandlerEffect<
479
- ROut,
480
- Self extends Definition.Any,
481
- Key extends keyof Self["methods"],
482
- > = WorkerRpcHandler<ROut, Method.Success<Self["methods"][Key]>>;
474
+ export type {
475
+ Api,
476
+ Definition,
477
+ HandlerEffect,
478
+ Handlers,
479
+ LayerOptions,
480
+ Method,
481
+ Methods,
482
+ NoReservedMethods,
483
+ Options,
484
+ ServerApi,
485
+ ServiceFreeSchema,
486
+ TagClass,
487
+ } from "./WorkerDefinition";
488
+
489
+ export { implement, method, Tag } from "./WorkerDefinition";
@@ -37,15 +37,10 @@ export namespace Method {
37
37
  ? [S.Schema.Type<Head>, ...ArgsFromSchemas<Tail>]
38
38
  : Array<S.Schema.Type<Args[number]>>;
39
39
 
40
- type EncodedArgsFromSchemas<Args extends ReadonlyArray<ServiceFreeSchema>> =
41
- Args extends readonly []
42
- ? []
43
- : Args extends readonly [
44
- infer Head extends ServiceFreeSchema,
45
- ...infer Tail extends ReadonlyArray<ServiceFreeSchema>,
46
- ]
47
- ? [S.Codec.Encoded<Head>, ...EncodedArgsFromSchemas<Tail>]
48
- : Array<S.Codec.Encoded<Args[number]>>;
40
+ /** Method schemas cross the wire through their canonical JSON codec. */
41
+ type EncodedArgsFromSchemas<Args extends ReadonlyArray<ServiceFreeSchema>> = {
42
+ [Index in keyof Args]: S.Json;
43
+ };
49
44
 
50
45
  export type Args<Self extends Any> = ArgsFromSchemas<Self["args"]>;
51
46
 
@@ -53,7 +48,7 @@ export namespace Method {
53
48
 
54
49
  export type Success<Self extends Any> = S.Schema.Type<Self["success"]>;
55
50
 
56
- export type EncodedSuccess<Self extends Any> = S.Codec.Encoded<Self["success"]>;
51
+ export type EncodedSuccess<Self extends Any> = S.Json;
57
52
  }
58
53
 
59
54
  export type Methods = Record<string, Method.Any>;
package/src/WorkersAi.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
 
15
15
  import * as Binding from "./Binding";
16
16
  import type { WorkerEnvironment } from "./Environment";
17
+ import * as ErrorMessage from "./internal/ErrorMessage";
17
18
 
18
19
  const expectedWorkersAiBinding = "Workers AI binding with run(), gateway(), and models()";
19
20
 
@@ -22,7 +23,11 @@ export class WorkersAiOperationError extends Data.TaggedError("WorkersAiOperatio
22
23
  readonly binding: string;
23
24
  readonly operation: string;
24
25
  readonly cause: unknown;
25
- }> {}
26
+ }> {
27
+ override get message(): string {
28
+ return `Workers AI ${this.operation} failed for binding "${this.binding}": ${ErrorMessage.causeMessage(this.cause)}`;
29
+ }
30
+ }
26
31
 
27
32
  /** Typed Workers AI binding definition. */
28
33
  export interface WorkersAiDefinition {
@@ -101,7 +106,7 @@ export interface WorkersAiClient<ModelList extends AiModelListType = AiModels> {
101
106
  readonly gateway: (
102
107
  gatewayId: string,
103
108
  ) => Effect.Effect<CloudflareAiGateway, WorkersAiOperationError>;
104
- readonly unsafeRaw: Effect.Effect<WorkersAiBinding<ModelList>>;
109
+ readonly rawUnsafe: Effect.Effect<WorkersAiBinding<ModelList>>;
105
110
  readonly definition: WorkersAiDefinition;
106
111
  }
107
112
 
@@ -177,10 +182,12 @@ export const embeddingResponse = (value: {
177
182
  export const makeClient =
178
183
  <ModelList extends AiModelListType = AiModels>(definition: WorkersAiDefinition) =>
179
184
  (ai: WorkersAiBinding<ModelList>): WorkersAiClient<ModelList> => {
180
- const run = ((model: string, input: Record<string, unknown>, options?: WorkersAiOptions) =>
181
- tryWorkersAiPromise(definition.binding, "run", () =>
182
- ai.run(model as string & {}, input, options),
183
- )) as WorkersAiClient<ModelList>["run"];
185
+ 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"];
184
191
 
185
192
  return {
186
193
  definition,
@@ -190,22 +197,25 @@ export const makeClient =
190
197
  () => ai.aiGatewayLogId,
191
198
  ),
192
199
  run,
193
- runEmbedding: (model, input, options) =>
194
- run(model, input, options).pipe(
195
- Effect.map((response) =>
196
- embeddingResponse(
197
- response as {
198
- readonly data?: ReadonlyArray<ReadonlyArray<number>>;
199
- readonly shape?: ReadonlyArray<number>;
200
- },
200
+ runEmbedding: Effect.fn("WorkersAi.runEmbedding")(
201
+ (model: string, input: WorkersAiEmbeddingInput, options?: WorkersAiOptions) =>
202
+ run(model, input as Record<string, unknown>, options).pipe(
203
+ Effect.map((response) =>
204
+ embeddingResponse(
205
+ response as {
206
+ readonly data?: ReadonlyArray<ReadonlyArray<number>>;
207
+ readonly shape?: ReadonlyArray<number>;
208
+ },
209
+ ),
201
210
  ),
202
211
  ),
203
- ),
204
- models: (params) =>
212
+ ),
213
+ models: Effect.fn("WorkersAi.models")((params?: WorkersAiModelsSearchParams) =>
205
214
  tryWorkersAiPromise(definition.binding, "models", () => ai.models(params)),
215
+ ),
206
216
  gateway: (gatewayId) =>
207
217
  tryWorkersAiSync(definition.binding, "gateway", () => ai.gateway(gatewayId)),
208
- unsafeRaw: Effect.succeed(ai),
218
+ rawUnsafe: Effect.succeed(ai),
209
219
  };
210
220
  };
211
221