effect-cf 0.34.0 → 0.35.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.
package/dist/index.mjs CHANGED
@@ -2,9 +2,9 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { a as WorkerEnvironment, i as WorkerConfig, n as violationsMessage, r as Environment_exports, t as causeMessage } from "./ErrorMessage-Byx2OgNd.mjs";
3
3
  import { n as Service$2, r as layer$19, t as Binding_exports } from "./Binding-C82jRhdr.mjs";
4
4
  import { n as Artifacts_exports } from "./Artifacts-CBxWGeZB.mjs";
5
- import { a as DurableObjectWebSocket_exports, c as DurableObjectStorage_exports, i as fromExecutionContext, n as DurableObjectState_exports, o as DurableWebSocketAttachmentError, r as fromDurableObjectState, s as fromWebSocket, t as DurableObjectState } from "./DurableObjectState-De4ZojhY.mjs";
6
- import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Function as Function$1, Layer, ManagedRuntime, Option, Predicate, Queue, References, Result, Schema, Tracer } from "effect";
7
- import { FetchHttpClient, HttpClient, HttpClientRequest, HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
5
+ import { a as DurableObjectWebSocket_exports, c as DurableObjectStorage_exports, i as fromExecutionContext, n as DurableObjectState_exports, o as DurableWebSocketAttachmentError, r as fromDurableObjectState, s as fromWebSocket, t as DurableObjectState } from "./DurableObjectState-DcqElJ4e.mjs";
6
+ import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, FiberSet, Function as Function$1, Layer, ManagedRuntime, Option, Predicate, Queue, References, Result, Schema, Tracer } from "effect";
7
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpEffect, HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
8
8
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
9
9
  import { D1Client } from "@effect/sql-d1";
10
10
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint } from "cloudflare:workers";
@@ -50,7 +50,7 @@ const embeddingResponse = (value) => ({
50
50
  data: value.data ?? [],
51
51
  [embeddingDimensionsKey]: value[embeddingDimensionsKey] ?? []
52
52
  });
53
- const decodeEmbeddingResponse = Schema.decodeUnknownOption(Schema.Struct({
53
+ const decodeEmbeddingResponse = Schema.decodeUnknownEffect(Schema.Struct({
54
54
  data: Schema.optional(Schema.Array(Schema.Array(Schema.Number))),
55
55
  [embeddingDimensionsKey]: Schema.optional(Schema.Array(Schema.Number))
56
56
  }));
@@ -62,11 +62,14 @@ const makeClient$15 = (definition) => (ai) => {
62
62
  definition,
63
63
  aiGatewayLogId: tryWorkersAiSync(definition.binding, "aiGatewayLogId", () => ai.aiGatewayLogId),
64
64
  run,
65
- runEmbedding: Effect.fn("WorkersAi.runEmbedding")((model, input, options) => {
66
- return run(model, input, options).pipe(Effect.map((response) => Option.match(decodeEmbeddingResponse(response), {
67
- onNone: () => embeddingResponse({}),
68
- onSome: embeddingResponse
65
+ runEmbedding: Effect.fn("WorkersAi.runEmbedding")(function* (model, input, options) {
66
+ const response = yield* run(model, input, options);
67
+ const decoded = yield* decodeEmbeddingResponse(response).pipe(Effect.mapError((cause) => new WorkersAiOperationError({
68
+ binding: definition.binding,
69
+ operation: "runEmbedding",
70
+ cause
69
71
  })));
72
+ return embeddingResponse(decoded);
70
73
  }),
71
74
  models: Effect.fn("WorkersAi.models")((params) => tryWorkersAiPromise(definition.binding, "models", () => ai.models(params))),
72
75
  gateway: (gatewayId) => tryWorkersAiSync(definition.binding, "gateway", () => ai.gateway(gatewayId)),
@@ -413,7 +416,11 @@ const queryRequest = (definition, sql, options) => {
413
416
  };
414
417
  const responseText = (definition, response, operation) => response.text.pipe(Effect.catch((cause) => Effect.fail(analyticsEngineQueryError(definition, operation, "Failed to read Analytics Engine SQL API response body", { cause }))));
415
418
  const executeQueryRequest = (definition, httpClient, sql, options) => Effect.gen(function* () {
416
- const response = yield* httpClient.execute(queryRequest(definition, sql, options)).pipe(Effect.mapError((cause) => analyticsEngineQueryError(definition, "query", "Analytics Engine SQL API request failed", { cause })));
419
+ const request = yield* Effect.try({
420
+ try: () => queryRequest(definition, sql, options),
421
+ catch: (cause) => analyticsEngineQueryError(definition, "query", "Invalid Analytics Engine SQL API URL", { cause })
422
+ });
423
+ const response = yield* httpClient.execute(request).pipe(Effect.mapError((cause) => analyticsEngineQueryError(definition, "query", "Analytics Engine SQL API request failed", { cause })));
417
424
  if (response.status < 200 || response.status >= 300) {
418
425
  const body = yield* responseText(definition, response, "queryErrorBody");
419
426
  return yield* Effect.fail(analyticsEngineQueryError(definition, "query", `Analytics Engine SQL API returned HTTP ${response.status}`, {
@@ -626,9 +633,10 @@ const layer$14 = Layer.sync(CacheStorage, () => makeClient$11(caches));
626
633
  //#endregion
627
634
  //#region src/CloudflareOtlp.ts
628
635
  /**
629
- * Worker fetch/RPC and Durable Object alarm/RPC handlers schedule a best-effort
630
- * flusher. It settles within two seconds; queues and other lifecycles must
631
- * flush explicitly. Failures are ignored so telemetry cannot fail user events.
636
+ * Worker fetch/queue/RPC and Durable Object alarm/RPC handlers schedule a
637
+ * best-effort flusher. It settles within two seconds; other lifecycles must
638
+ * flush explicitly. Scheduled flush failures cannot fail user events. Exporter
639
+ * layer finalizers run separately and follow their configured shutdown timeout.
632
640
  */
633
641
  var CloudflareOtlp_exports = /* @__PURE__ */ __exportAll({
634
642
  layer: () => layer$13,
@@ -713,6 +721,8 @@ const layerProtobuf = (options = {}) => layer$13({
713
721
  *
714
722
  * Provide this layer at runtime scope for long-lived metrics aggregation, or at
715
723
  * handler scope when traces/logs should flush as the Cloudflare event finishes.
724
+ * A handler-scoped exporter can delay scope closure while its own finalizer
725
+ * flushes; the two-second background flush limit does not bound layer shutdown.
716
726
  */
717
727
  const layerWorker = (options = {}) => makeLayer(options, {
718
728
  "cloudflare.resource_type": "worker",
@@ -1474,7 +1484,13 @@ const makeClient$8 = (definition) => {
1474
1484
  return (service) => {
1475
1485
  const fetch = Effect.fn("ServiceBinding.fetch")(function* (input, init) {
1476
1486
  return yield* Effect.tryPromise({
1477
- try: () => service.fetch(input, init),
1487
+ try: (signal) => {
1488
+ const callerSignal = init?.signal !== void 0 ? init.signal : input instanceof Request ? input.signal : void 0;
1489
+ return service.fetch(input, {
1490
+ ...init,
1491
+ signal: callerSignal === null || callerSignal === void 0 ? signal : AbortSignal.any([callerSignal, signal])
1492
+ });
1493
+ },
1478
1494
  catch: (cause) => new ServiceBindingFetchError({
1479
1495
  binding: definition.binding,
1480
1496
  cause
@@ -1515,7 +1531,11 @@ const makeClient$8 = (definition) => {
1515
1531
  const scopedCall = Effect.fnUntraced(function* (method, ...args) {
1516
1532
  const methodName = String(method);
1517
1533
  const result = yield* rpc(method, ...args);
1518
- const value = yield* scoped(result);
1534
+ const value = yield* scoped(result).pipe(Effect.mapError((cause) => new ServiceBindingRpcError({
1535
+ binding: definition.binding,
1536
+ method: methodName,
1537
+ cause
1538
+ })));
1519
1539
  return yield* decodeSuccess$2(methodName, value);
1520
1540
  }, (effect, ...args) => withRpcClientSpan(effect, definition.binding, String(args[0])));
1521
1541
  const directMethods = makeDirectMethods$1(definition.definition, call);
@@ -1674,39 +1694,42 @@ var WorkerContext = class extends Context.Service()("effect-cf/WorkerContext") {
1674
1694
  var NativeRequest = class extends Context.Service()("effect-cf/NativeRequest") {};
1675
1695
  const isWebSocketUpgrade = (request) => request.headers.get("Upgrade")?.toLowerCase() === "websocket";
1676
1696
  const reservedMethodNames$2 = /* @__PURE__ */ new Set([
1677
- "constructor",
1678
- "dup",
1679
- "fetch",
1680
- "connect",
1697
+ ...reservedMethodNames$4,
1681
1698
  "queue",
1682
1699
  "scheduled",
1683
1700
  "tail",
1684
1701
  "tailStream",
1685
1702
  "test",
1686
- "trace",
1687
- "alarm",
1688
- "webSocketMessage",
1689
- "webSocketClose",
1690
- "webSocketError"
1703
+ "trace"
1691
1704
  ]);
1692
1705
  /** Override to instrument the whole event, then call super[RunSymbol]. */
1693
1706
  const RunSymbol$1 = Symbol.for("effect-cf/Worker/run");
1694
1707
  const FetchSymbol = Symbol.for("effect-cf/Worker/fetch");
1695
- const renderFetchSuccess = (effect) => Effect.suspend(() => {
1708
+ const renderFetchSuccess = (effect, eventLayer) => Effect.suspend(() => {
1696
1709
  let nativeResponse;
1697
1710
  let webResponse;
1698
- const handled = HttpEffect.toHandled(Effect.flatMap(effect, (response) => {
1711
+ const httpApp = Effect.flatMap(effect, (response) => {
1699
1712
  if (response instanceof Response) {
1700
1713
  nativeResponse = response;
1701
1714
  return Effect.succeed(HttpServerResponse.empty());
1702
1715
  }
1703
1716
  return Effect.succeed(response);
1704
- }), (request, response) => nativeResponse !== void 0 ? Effect.void : Effect.map(Effect.context(), (context) => {
1717
+ });
1718
+ const handleResponse = (request, response) => nativeResponse !== void 0 ? Effect.void : Effect.map(Effect.context(), (context) => {
1705
1719
  webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
1706
1720
  withoutBody: request.method === "HEAD",
1707
1721
  context
1708
1722
  });
1723
+ });
1724
+ const telemetryMiddleware = HttpMiddleware.make((self) => Effect.gen(function* () {
1725
+ yield* Effect.addFinalizer(() => scheduleTelemetryFlush$1);
1726
+ return yield* self;
1709
1727
  }));
1728
+ const handled = eventLayer === void 0 ? HttpEffect.toHandled(httpApp, handleResponse, telemetryMiddleware) : Effect.flatMap(References.TracerEnabled, (tracerEnabled) => HttpEffect.toHandled(httpApp, handleResponse, HttpMiddleware.make((self) => Effect.provideService(Effect.gen(function* () {
1729
+ const context = yield* Layer.build(Layer.fresh(eventLayer));
1730
+ yield* Effect.addFinalizer(() => Effect.provideContext(scheduleTelemetryFlush$1, context));
1731
+ return yield* HttpMiddleware.tracer(self).pipe(Effect.onExit(() => Effect.yieldNow), Effect.provideContext(context));
1732
+ }), References.TracerEnabled, tracerEnabled))).pipe(Effect.withTracerEnabled(false)));
1710
1733
  const captured = () => nativeResponse ?? webResponse ?? new Response(null, { status: 500 });
1711
1734
  return Effect.matchCause(handled, {
1712
1735
  onFailure: captured,
@@ -1715,13 +1738,13 @@ const renderFetchSuccess = (effect) => Effect.suspend(() => {
1715
1738
  });
1716
1739
  const isWorkerOptions = (options) => !Effect.isEffect(options);
1717
1740
  /**
1718
- * Drains buffered OTLP telemetry once the fetch handler has produced its
1719
- * response.
1741
+ * Drains buffered OTLP telemetry when the fetch Effect scope closes. Effect
1742
+ * response streams transfer that scope to the body and schedule the flush when
1743
+ * the body finishes or is canceled.
1720
1744
  *
1721
- * The flush is handed to `ctx.waitUntil` so it never delays the response
1722
- * (including streaming bodies still being consumed). It is a no-op when the
1723
- * context does not contain an `OtlpExporter.Flusher`. The handed-off effect is
1724
- * best-effort and silently settles within two seconds.
1745
+ * The flush is handed to `ctx.waitUntil` instead of being awaited by the
1746
+ * handler. It is a no-op when the context does not contain an
1747
+ * `OtlpExporter.Flusher`, and silently settles within two seconds.
1725
1748
  */
1726
1749
  const scheduleTelemetryFlush$1 = scheduleTelemetryFlush$2((flush) => Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flush)));
1727
1750
  function make$9(layer, optionsOrFetch) {
@@ -1733,7 +1756,7 @@ function make$9(layer, optionsOrFetch) {
1733
1756
  this.runtime = makeEntrypointRuntime(layer, env, Layer.mergeAll(Layer.succeed(ExecutionContext, ctx), Layer.succeed(WorkerContext, fromExecutionContext(ctx))));
1734
1757
  }
1735
1758
  [RunSymbol$1](effect, runOptions = {}) {
1736
- const eventLayer = options.eventLayer;
1759
+ const eventLayer = runOptions.event === "fetch" ? void 0 : options.eventLayer;
1737
1760
  const parent = runOptions.rpc?.parent;
1738
1761
  const parentSpan = parent === void 0 ? void 0 : Tracer.externalSpan(parent);
1739
1762
  if (eventLayer === void 0) return runEventPromise(this.runtime, effect, void 0, parentSpan);
@@ -1747,13 +1770,13 @@ function make$9(layer, optionsOrFetch) {
1747
1770
  if (fetchHandler === void 0) return Promise.resolve(new Response("Not Found", { status: 404 }));
1748
1771
  const ctx = requestContext ?? this.ctx;
1749
1772
  const requestServices = Layer.mergeAll(Layer.succeed(NativeRequest, request), Layer.succeed(HttpServerRequest.HttpServerRequest, HttpServerRequest.fromWeb(request)), Layer.succeed(ExecutionContext, ctx), Layer.succeed(WorkerContext, fromExecutionContext(ctx)));
1750
- return this[RunSymbol$1](renderFetchSuccess(fetchHandler).pipe(Effect.tap(() => scheduleTelemetryFlush$1), Effect.provide(requestServices)), { event: "fetch" });
1773
+ return this[RunSymbol$1](renderFetchSuccess(fetchHandler, options.eventLayer).pipe(Effect.provide(requestServices)), { event: "fetch" });
1751
1774
  }
1752
1775
  queue(batch) {
1753
1776
  const queueHandler = options.queue;
1754
1777
  if (queueHandler === void 0) return Promise.resolve();
1755
1778
  const messages = batch.messages.map((message) => fromMessage(message, message.body));
1756
- return this[RunSymbol$1](queueHandler(fromMessageBatch(batch, messages)), { event: "queue" });
1779
+ return this[RunSymbol$1](queueHandler(fromMessageBatch(batch, messages)).pipe(Effect.onExit(() => scheduleTelemetryFlush$1)), { event: "queue" });
1757
1780
  }
1758
1781
  }
1759
1782
  defineEntrypointRpcMethods("Worker", EffectWorker.prototype, options.rpc, reservedMethodNames$2, (self, effect, rpc) => self[RunSymbol$1](effect, {
@@ -1812,7 +1835,13 @@ const makeClient$7 = (definition) => {
1812
1835
  const getByName = (name, options) => Effect.sync(() => namespace.getByName(name, options));
1813
1836
  const jurisdiction = (value) => Effect.sync(() => namespace.jurisdiction(value));
1814
1837
  const fetch = (stub, input, init) => Effect.tryPromise({
1815
- try: () => stub.fetch(input, init),
1838
+ try: (signal) => {
1839
+ const callerSignal = init?.signal !== void 0 ? init.signal : input instanceof Request ? input.signal : void 0;
1840
+ return stub.fetch(input, {
1841
+ ...init,
1842
+ signal: callerSignal === null || callerSignal === void 0 ? signal : AbortSignal.any([callerSignal, signal])
1843
+ });
1844
+ },
1816
1845
  catch: (cause) => new DurableObjectFetchError({
1817
1846
  binding: definition.binding,
1818
1847
  cause
@@ -1852,7 +1881,11 @@ const makeClient$7 = (definition) => {
1852
1881
  const scopedCall = Effect.fnUntraced(function* (stub, method, ...args) {
1853
1882
  const methodName = String(method);
1854
1883
  const result = yield* rpc(stub, method, ...args);
1855
- const value = yield* scoped(result);
1884
+ const value = yield* scoped(result).pipe(Effect.mapError((cause) => new DurableObjectRpcError({
1885
+ binding: definition.binding,
1886
+ method: methodName,
1887
+ cause
1888
+ })));
1856
1889
  return yield* decodeSuccess$1(methodName, value);
1857
1890
  }, (effect, ...args) => withRpcClientSpan(effect, definition.binding, String(args[1])));
1858
1891
  const directMethods = makeDirectMethods(definition.definition, {
@@ -2023,18 +2056,16 @@ var DurableObject_exports = /* @__PURE__ */ __exportAll({
2023
2056
  make: () => make$7,
2024
2057
  method: () => method
2025
2058
  });
2026
- const reservedMethodNames = /* @__PURE__ */ new Set([
2027
- "constructor",
2028
- "dup",
2029
- "fetch",
2030
- "alarm",
2031
- "webSocketMessage",
2032
- "webSocketClose",
2033
- "webSocketError"
2034
- ]);
2059
+ const reservedMethodNames = reservedMethodNames$4;
2035
2060
  /** Override to instrument the whole event, then call super[RunSymbol]. */
2036
2061
  const RunSymbol = Symbol.for("effect-cf/DurableObject/run");
2037
2062
  const scheduleTelemetryFlush = scheduleTelemetryFlush$2((flush) => Effect.flatMap(DurableObjectState, (state) => state.waitUntil(flush)));
2063
+ /**
2064
+ * Builds a Durable Object whose base `layer` lives for the in-memory instance.
2065
+ * Cloudflare does not expose an eviction or shutdown hook, so finalizers in
2066
+ * that layer are not guaranteed to run. Put resources that require timely
2067
+ * release in `eventLayer`, whose scope closes after each handled event.
2068
+ */
2038
2069
  const make$7 = (layer, options = {}) => {
2039
2070
  class EffectDurableObject extends DurableObject {
2040
2071
  runtime;
@@ -2411,7 +2442,6 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2411
2442
  const disconnects = yield* Queue.make();
2412
2443
  const connectionsBySocket = /* @__PURE__ */ new Map();
2413
2444
  const connectionsById = /* @__PURE__ */ new Map();
2414
- const clientIds = /* @__PURE__ */ new Set();
2415
2445
  const resetSockets = /* @__PURE__ */ new WeakSet();
2416
2446
  const declarationsById = /* @__PURE__ */ new Map();
2417
2447
  const declarationsByTag = /* @__PURE__ */ new Map();
@@ -2427,14 +2457,6 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2427
2457
  declarationsByTag.set(declaration.rpcTag, declaration);
2428
2458
  }
2429
2459
  const reserveClientId = (socket) => {
2430
- const attachment = readAttachment(socket.raw, attachmentKey);
2431
- if (attachment._tag === "Invalid") throw new Error("Invalid Durable Object RPC websocket attachment metadata");
2432
- if (attachment._tag === "Valid") {
2433
- const metadata = attachment.metadata;
2434
- nextClientId = Math.max(nextClientId, metadata.clientId + 1);
2435
- writeAttachment(socket.raw, attachmentKey, metadata);
2436
- return metadata;
2437
- }
2438
2460
  const id = nextClientId++;
2439
2461
  const created = {
2440
2462
  version: legacyAttachmentVersion,
@@ -2487,7 +2509,6 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2487
2509
  for (const subscription of restoredSubscriptions) if (!addSubscription(connection, subscription)) throw new Error("Duplicate restored resumable RPC stream subscription identity");
2488
2510
  connectionsBySocket.set(socket.raw, connection);
2489
2511
  connectionsById.set(connection.id, connection);
2490
- clientIds.add(connection.id);
2491
2512
  nextClientId = Math.max(nextClientId, connection.id + 1);
2492
2513
  return connection;
2493
2514
  };
@@ -2524,7 +2545,6 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2524
2545
  if (connection === void 0) return;
2525
2546
  connectionsBySocket.delete(socket.raw);
2526
2547
  connectionsById.delete(connection.id);
2527
- clientIds.delete(connection.id);
2528
2548
  activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
2529
2549
  connection.requestIds.clear();
2530
2550
  connection.nonResumableRequestIds.clear();
@@ -2743,7 +2763,7 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2743
2763
  end: (clientId) => Effect.sync(() => {
2744
2764
  connectionsById.get(clientId)?.socket.raw.close();
2745
2765
  }),
2746
- clientIds: Effect.sync(() => clientIds),
2766
+ clientIds: Effect.sync(() => new Set(connectionsById.keys())),
2747
2767
  initialMessage: Effect.succeedNone,
2748
2768
  supportsAck: true,
2749
2769
  supportsTransferables: false,
@@ -2877,13 +2897,9 @@ const writeAttachment = (socket, key, metadata) => {
2877
2897
  const current = socket.deserializeAttachment();
2878
2898
  if (current !== null && current !== void 0 && !Predicate.isObject(current)) throw new Error("DurableObjectRpcWebSocket requires object-shaped websocket attachments so adapter metadata can preserve application fields");
2879
2899
  const attachment = Predicate.isObject(current) ? current : {};
2880
- const currentMetadata = Predicate.hasProperty(attachment, key) && Predicate.isObject(attachment[key]) ? attachment[key] : {};
2881
2900
  socket.serializeAttachment({
2882
2901
  ...attachment,
2883
- [key]: {
2884
- ...currentMetadata,
2885
- ...metadata
2886
- }
2902
+ [key]: metadata
2887
2903
  });
2888
2904
  };
2889
2905
  const makeHeartbeatController = (state, serialization, policy) => Effect.gen(function* () {
@@ -3835,20 +3851,29 @@ const exposeCloudflareNonRetryableError = (effect) => Effect.catchCause(effect,
3835
3851
  const error = Cause.findErrorOption(cause);
3836
3852
  return Option.isSome(error) && error.value instanceof WorkflowStepNonRetryableError ? Effect.die(toCloudflareNonRetryableError(error.value)) : Effect.failCause(cause);
3837
3853
  });
3838
- const fromWorkflowStep = (step, runPromise) => ({
3854
+ const fromWorkflowStep = (step) => ({
3839
3855
  raw: step,
3840
- do: (name, effect, config) => Effect.context().pipe(Effect.flatMap((context) => Effect.tryPromise({
3841
- try: () => {
3842
- const run = (stepContext) => runPromise(exposeCloudflareNonRetryableError(Effect.scoped(Effect.provideService(Effect.provideContext(effect, context), WorkflowStepContext$1, stepContext))));
3843
- const rawStep = step;
3844
- return config === void 0 ? rawStep.do(name, run) : rawStep.do(name, config, run);
3845
- },
3846
- catch: (cause) => new WorkflowStepError({
3847
- step: name,
3848
- operation: "do",
3849
- cause
3850
- })
3851
- }))),
3856
+ do: (name, effect, config) => Effect.gen(function* () {
3857
+ const context = yield* Effect.context();
3858
+ return yield* Effect.scoped(Effect.gen(function* () {
3859
+ const runPromise = yield* FiberSet.makeRuntimePromise();
3860
+ return yield* Effect.tryPromise({
3861
+ try: () => {
3862
+ const run = (stepContext) => {
3863
+ const stepEffect = Effect.scoped(Effect.provideService(effect, WorkflowStepContext$1, stepContext));
3864
+ return runPromise(exposeCloudflareNonRetryableError(Effect.provideContext(stepEffect, context)));
3865
+ };
3866
+ const rawStep = step;
3867
+ return config === void 0 ? rawStep.do(name, run) : rawStep.do(name, config, run);
3868
+ },
3869
+ catch: (cause) => new WorkflowStepError({
3870
+ step: name,
3871
+ operation: "do",
3872
+ cause
3873
+ })
3874
+ });
3875
+ }));
3876
+ }),
3852
3877
  sleep: (name, duration) => Effect.tryPromise({
3853
3878
  try: () => step.sleep(name, duration),
3854
3879
  catch: (cause) => new WorkflowStepError({
@@ -3884,7 +3909,7 @@ const make = (layer, options) => {
3884
3909
  this.runtime = makeEntrypointRuntime(layer, env, Layer.mergeAll(Layer.succeed(ExecutionContext, ctx), Layer.succeed(WorkerContext, fromExecutionContext(ctx))));
3885
3910
  }
3886
3911
  run(event, step) {
3887
- const workflowServices = Layer.mergeAll(Layer.succeed(WorkflowEvent$1, fromWorkflowEvent(event)), Layer.succeed(WorkflowStep$1, fromWorkflowStep(step, (effect) => this.runtime.runPromise(effect))));
3912
+ const workflowServices = Layer.mergeAll(Layer.succeed(WorkflowEvent$1, fromWorkflowEvent(event)), Layer.succeed(WorkflowStep$1, fromWorkflowStep(step)));
3888
3913
  return runEventPromise(this.runtime, options.run(event.payload), workflowServices);
3889
3914
  }
3890
3915
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -572,7 +572,14 @@ const executeQueryRequest = (
572
572
  options?: AnalyticsEngineQueryOptions,
573
573
  ) =>
574
574
  Effect.gen(function* () {
575
- const response = yield* httpClient.execute(queryRequest(definition, sql, options)).pipe(
575
+ const request = yield* Effect.try({
576
+ try: () => queryRequest(definition, sql, options),
577
+ catch: (cause) =>
578
+ analyticsEngineQueryError(definition, "query", "Invalid Analytics Engine SQL API URL", {
579
+ cause,
580
+ }),
581
+ });
582
+ const response = yield* httpClient.execute(request).pipe(
576
583
  Effect.mapError((cause) =>
577
584
  analyticsEngineQueryError(definition, "query", "Analytics Engine SQL API request failed", {
578
585
  cause,
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Worker fetch/RPC and Durable Object alarm/RPC handlers schedule a best-effort
3
- * flusher. It settles within two seconds; queues and other lifecycles must
4
- * flush explicitly. Failures are ignored so telemetry cannot fail user events.
2
+ * Worker fetch/queue/RPC and Durable Object alarm/RPC handlers schedule a
3
+ * best-effort flusher. It settles within two seconds; other lifecycles must
4
+ * flush explicitly. Scheduled flush failures cannot fail user events. Exporter
5
+ * layer finalizers run separately and follow their configured shutdown timeout.
5
6
  */
6
7
  import { ConfigProvider, Effect, Layer, Option } from "effect";
7
8
  import type * as Tracer from "effect/Tracer";
@@ -207,6 +208,8 @@ export const layerProtobuf = (
207
208
  *
208
209
  * Provide this layer at runtime scope for long-lived metrics aggregation, or at
209
210
  * handler scope when traces/logs should flush as the Cloudflare event finishes.
211
+ * A handler-scoped exporter can delay scope closure while its own finalizer
212
+ * flushes; the two-second background flush limit does not bound layer shutdown.
210
213
  */
211
214
  export const layerWorker = (options: WorkerLayerOptions = {}): Layer.Layer<OtlpExporter.Flusher> =>
212
215
  makeLayer(options, {
@@ -100,6 +100,17 @@ export class WorkspaceAssetsError extends Schema.TaggedError<WorkspaceAssetsErro
100
100
  },
101
101
  ) {}
102
102
 
103
+ /** Schema-serializable error while acquiring the Computer workspace client. */
104
+ export class WorkspaceAcquireError extends Schema.TaggedError<WorkspaceAcquireError>()(
105
+ "WorkspaceAcquireError",
106
+ {
107
+ operation: Schema.String,
108
+ message: Schema.String,
109
+ code: Schema.optionalKey(Schema.String),
110
+ cause: Schema.optionalKey(Schema.Defect({ includeStack: true })),
111
+ },
112
+ ) {}
113
+
103
114
  export type WorkspaceReadFileOptions = Omit<CloudflareReadFileOptions, "encoding">;
104
115
  export type ReadTextFileOptions = WorkspaceReadFileOptions;
105
116
  export type ListDirectoryOptions = CloudflareReaddirOptions;
@@ -469,6 +480,21 @@ const execError = (operation: string, cause: unknown) => {
469
480
  return WorkspaceExecError.make(properties);
470
481
  };
471
482
 
483
+ const acquireError = (operation: string, cause: unknown) => {
484
+ const properties: WorkspaceOperationErrorProperties = {
485
+ operation,
486
+ message: errorMessage(cause),
487
+ cause,
488
+ };
489
+ const code = errorCode(cause);
490
+
491
+ if (code !== undefined) {
492
+ properties.code = code;
493
+ }
494
+
495
+ return WorkspaceAcquireError.make(properties);
496
+ };
497
+
472
498
  const assetsError = (operation: string, path: string, cause: unknown) => {
473
499
  const properties: WorkspaceFsErrorProperties = {
474
500
  operation,
@@ -887,9 +913,6 @@ const wrapRun = <Encoding extends WorkspaceExecEncoding>(
887
913
  return { id: handle.id, backend: handle.backend, events, result, kill };
888
914
  };
889
915
 
890
- const releaseRun = (handle: CloudflareWorkspaceRuntimeExecHandle<"utf8" | undefined>) =>
891
- Effect.sync(() => handle[Symbol.dispose]());
892
-
893
916
  const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntime => {
894
917
  const execImplementation = Effect.fn("ComputerWorkspace.runtime.exec", {
895
918
  attributes: { family: "runtime", operation: "exec" },
@@ -905,7 +928,7 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
905
928
  : undefined;
906
929
 
907
930
  yield* Effect.annotateCurrentSpan({ backend: options?.backend, runId: options?.id });
908
- const handle = yield* Effect.acquireRelease(
931
+ const handle = yield* Effect.acquireDisposable(
909
932
  Effect.tryPromise({
910
933
  try: () => {
911
934
  if (Predicate.isString(source)) {
@@ -926,7 +949,6 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
926
949
  },
927
950
  catch: (cause) => execError("exec", cause),
928
951
  }),
929
- releaseRun,
930
952
  );
931
953
 
932
954
  // SAFETY: runtimeExecOptions carries Encoding to Cloudflare's corresponding handle overload.
@@ -945,12 +967,11 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
945
967
  options?: WorkspaceGetExecOptions<Encoding>,
946
968
  ) {
947
969
  yield* Effect.annotateCurrentSpan({ backend: options?.backend, runId: id });
948
- const handle = yield* Effect.acquireRelease(
970
+ const handle = yield* Effect.acquireDisposable(
949
971
  Effect.tryPromise({
950
972
  try: () => client.runtime.getExec(id, runtimeGetOptions(options)),
951
973
  catch: (cause) => execError("getExec", cause),
952
974
  }),
953
- releaseRun,
954
975
  );
955
976
 
956
977
  // SAFETY: runtimeGetOptions carries Encoding to Cloudflare's corresponding handle overload.
@@ -1169,25 +1190,31 @@ export class ComputerWorkspace extends Context.Service<
1169
1190
  * Acquires the single workspace registered by `withComputerWorkspace` and
1170
1191
  * releases its RPC client when the layer scope closes.
1171
1192
  */
1172
- static readonly layer: Layer.Layer<ComputerWorkspace, never, DurableObjectState> = Layer.effect(
1173
- ComputerWorkspace,
1174
- Effect.gen(function* () {
1175
- const state = yield* DurableObjectState;
1176
- const host = HostRegistry.lookup(state.raw);
1177
-
1178
- if (host === undefined) {
1179
- return yield* Effect.die(
1180
- "ComputerWorkspace.layer requires a Durable Object class built with withComputerWorkspace",
1193
+ static readonly layer: Layer.Layer<ComputerWorkspace, WorkspaceAcquireError, DurableObjectState> =
1194
+ Layer.effect(
1195
+ ComputerWorkspace,
1196
+ Effect.gen(function* () {
1197
+ const state = yield* DurableObjectState;
1198
+ const host = HostRegistry.lookup(state.raw);
1199
+
1200
+ if (host === undefined) {
1201
+ return yield* Effect.die(
1202
+ "ComputerWorkspace.layer requires a Durable Object class built with withComputerWorkspace",
1203
+ );
1204
+ }
1205
+
1206
+ const computer = yield* Effect.tryPromise({
1207
+ try: () => import("@cloudflare/computer"),
1208
+ catch: (cause) => acquireError("import", cause),
1209
+ });
1210
+ const client = yield* Effect.acquireDisposable(
1211
+ Effect.tryPromise({
1212
+ try: () => computer.getWorkspace(host),
1213
+ catch: (cause) => acquireError("getWorkspace", cause),
1214
+ }),
1181
1215
  );
1182
- }
1183
-
1184
- const computer = yield* Effect.promise(() => import("@cloudflare/computer"));
1185
- const client = yield* Effect.acquireRelease(
1186
- Effect.promise(() => computer.getWorkspace(host)),
1187
- (workspace) => Effect.sync(() => workspace[Symbol.dispose]()),
1188
- );
1189
1216
 
1190
- return ComputerWorkspace.of(fromWorkspaceClient(client));
1191
- }),
1192
- );
1217
+ return ComputerWorkspace.of(fromWorkspaceClient(client));
1218
+ }),
1219
+ );
1193
1220
  }
@@ -5,20 +5,13 @@ import { NativeRequest } from "./Worker";
5
5
  import { WorkerEnvironment, type WorkerEnv } from "./Environment";
6
6
  import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState";
7
7
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
8
+ import * as RpcDefinition from "./RpcDefinition";
8
9
  import * as Entrypoint from "./internal/Entrypoint";
9
10
  import * as Runtime from "./internal/Runtime";
10
11
  import * as Telemetry from "./internal/Telemetry";
11
12
  import type { ReceiverOptions, RpcInvocationInfo } from "./RpcTracing";
12
13
 
13
- const reservedMethodNames = new Set<string>([
14
- "constructor",
15
- "dup",
16
- "fetch",
17
- "alarm",
18
- "webSocketMessage",
19
- "webSocketClose",
20
- "webSocketError",
21
- ]);
14
+ const reservedMethodNames: ReadonlySet<string> = RpcDefinition.reservedMethodNames;
22
15
 
23
16
  type RuntimeContext<ROut> = DurableObjectState | WorkerEnvironment | ROut;
24
17
 
@@ -167,6 +160,12 @@ export type DurableObjectClass<Rpc extends DurableObjectRpc<ROut>, ROut> = new (
167
160
  ): Promise<A>;
168
161
  };
169
162
 
163
+ /**
164
+ * Builds a Durable Object whose base `layer` lives for the in-memory instance.
165
+ * Cloudflare does not expose an eviction or shutdown hook, so finalizers in
166
+ * that layer are not guaranteed to run. Put resources that require timely
167
+ * release in `eventLayer`, whose scope closes after each handled event.
168
+ */
170
169
  export const make = <
171
170
  ROut,
172
171
  LayerError,
@@ -250,7 +250,7 @@ export type DurableObjectNamespaceEffectClient<
250
250
  stub: DurableObjectStubClient<Api>,
251
251
  method: Method,
252
252
  ...args: StubMethodArgs<Api, Method>
253
- ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, unknown, Scope.Scope>;
253
+ ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, DurableObjectRpcError, Scope.Scope>;
254
254
  /**
255
255
  * Exposes the underlying native Durable Object namespace binding.
256
256
  *
@@ -300,7 +300,11 @@ export type DurableObjectNamespaceStaticClient<
300
300
  stub: DurableObjectStubClient<Api>,
301
301
  method: Method,
302
302
  ...args: StubMethodArgs<Api, Method>
303
- ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, unknown, Scope.Scope | R>;
303
+ ) => Effect.Effect<
304
+ Awaited<StubMethodSuccess<Api, Method>>,
305
+ DurableObjectRpcError,
306
+ Scope.Scope | R
307
+ >;
304
308
  readonly rawUnsafe: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
305
309
  };
306
310
 
@@ -351,7 +355,22 @@ export const makeClient = <
351
355
 
352
356
  const fetch = (stub: StubClient, input: RequestInfo | URL, init?: RequestInit) =>
353
357
  Effect.tryPromise({
354
- try: () => stub.fetch(input, init),
358
+ try: (signal) => {
359
+ const callerSignal =
360
+ init?.signal !== undefined
361
+ ? init.signal
362
+ : input instanceof Request
363
+ ? input.signal
364
+ : undefined;
365
+
366
+ return stub.fetch(input, {
367
+ ...init,
368
+ signal:
369
+ callerSignal === null || callerSignal === undefined
370
+ ? signal
371
+ : AbortSignal.any([callerSignal, signal]),
372
+ });
373
+ },
355
374
  catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
356
375
  });
357
376
 
@@ -456,10 +475,19 @@ export const makeClient = <
456
475
  stub: StubClient,
457
476
  method: Method,
458
477
  ...args: StubMethodArgs<Api, Method>
459
- ): Effect.fn.Return<StubMethodSuccess<Api, Method>, unknown, Scope.Scope> {
478
+ ): Effect.fn.Return<StubMethodSuccess<Api, Method>, DurableObjectRpcError, Scope.Scope> {
460
479
  const methodName = String(method);
461
480
  const result = yield* rpc(stub, method, ...args);
462
- const value = yield* CloudflareRpc.scoped(result);
481
+ const value = yield* CloudflareRpc.scoped(result).pipe(
482
+ Effect.mapError(
483
+ (cause) =>
484
+ new DurableObjectRpcError({
485
+ binding: definition.binding,
486
+ method: methodName,
487
+ cause,
488
+ }),
489
+ ),
490
+ );
463
491
 
464
492
  return yield* decodeSuccess<Method>(methodName, value);
465
493
  },