effect-cf 0.28.0 → 0.29.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#108](https://github.com/danieljvdm/effect-cf/pull/108) [`735b6d9`](https://github.com/danieljvdm/effect-cf/commit/735b6d95e4b62c7ade598a7f18ba2d8b4ee60f87) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Require the Effect 4.0.0-rc.110 family — dependency ranges move from `^4.0.0-beta.107 <4.0.0-rc.0` to `^4.0.0-rc.110`.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#66](https://github.com/danieljvdm/effect-cf/pull/66) [`fe34f95`](https://github.com/danieljvdm/effect-cf/commit/fe34f95abc92f4637968186aeebbb8033157c041) Thanks [@danieljvdm](https://github.com/danieljvdm)! - `DurableObjectStorage.transaction` and `transactionSync` now recover the aborting `Exit` by identity instead of re-deriving its type from the untyped `catch` binding. Failures raised inside a transaction keep the same typed error channel, and a failure thrown by the platform itself is still reported as `StorageOperationError`. No public API change.
12
+
13
+ ## 0.28.1
14
+
15
+ ### Patch Changes
16
+
17
+ - [#105](https://github.com/danieljvdm/effect-cf/pull/105) [`f5dfd5e`](https://github.com/danieljvdm/effect-cf/commit/f5dfd5e2c79b257cc760dc0b2cdf5a2901b0b295) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Bound internally scheduled telemetry flushes so exporter retries cannot extend Worker or Durable Object event lifetimes indefinitely.
18
+
19
+ - [#106](https://github.com/danieljvdm/effect-cf/pull/106) [`c0fbb85`](https://github.com/danieljvdm/effect-cf/commit/c0fbb8535479262741e6844935e9ff9d508f6b56) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Silently absorb failures raised while scheduling automatic OTLP telemetry flushes so platform boundary defects cannot feed back into the configured exporter. Document the two-second best-effort boundary and keep explicit queue flush policy application-owned.
20
+
3
21
  ## 0.28.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -357,6 +357,12 @@ Producers should usually use `const queue = yield* AvatarQueue` and then call `q
357
357
 
358
358
  Queue handlers run inline failures through Cloudflare's normal retry path. If background work scheduled with `WorkerContext.waitUntil(...)` should also make the batch retry, use `WorkerContext.waitUntilPropagating(...)` or `waitUntil(..., { mode: "propagate" })`; the default `waitUntil` mode observes and logs failures without rejecting the native `waitUntil` promise.
359
359
 
360
+ effect-cf automatically schedules best-effort OTLP flushes only for Worker
361
+ fetch/RPC and Durable Object alarm/RPC handlers. Those internal flushes are
362
+ capped at two seconds. Queue handlers do not flush telemetry automatically;
363
+ applications that explicitly flush in a queue handler own that flush's timeout
364
+ and failure policy.
365
+
360
366
  ## Cache Example
361
367
 
362
368
  `Cache.layer` exposes Cloudflare's global Cache API as an Effect service. Cache misses are represented by `Option.none()`, and named caches are available through `open(...)`.
@@ -1,5 +1,5 @@
1
1
  import { ComputerArtifactsClient } from "./ComputerArtifacts.mjs";
2
- import { t as DurableObjectState } from "./DurableObjectState-B3uFY1eM.mjs";
2
+ import { t as DurableObjectState } from "./DurableObjectState-OupNMMUl.mjs";
3
3
  import { Context, Effect, Layer, Schema, Scope, Stream } from "effect";
4
4
  import { CommitView, GitClient, GitCloneOptions, GitCommitOptions, GitDiffOptions, GitFetchOptions, GitLogOptions, GitUpdateRefOptions } from "@cloudflare/computer/git";
5
5
  import { ExecSyncResult, RuntimeExecOptions, RuntimeGetOptions, RuntimeKillOptions, ShellValue, SkippedEntry, ThinkWorkspaceCompatibility, WorkspaceClient, WorkspaceRuntimeResult, WorkspaceRuntimeValue as WorkspaceRuntimeValue$1 } from "@cloudflare/computer";
@@ -1,4 +1,4 @@
1
- import { t as DurableObjectState } from "./DurableObjectState-BjkK2kwH.mjs";
1
+ import { t as DurableObjectState } from "./DurableObjectState-DnnLoHV-.mjs";
2
2
  import { fromClient } from "./ComputerArtifacts.mjs";
3
3
  import { t as lookup } from "./ComputerWorkspaceHostRegistry-CY5Fv4Ng.mjs";
4
4
  import { Context, Effect, Layer, Predicate, Schema, Stream } from "effect";
@@ -99,24 +99,28 @@ const fromDurableObjectStorage = (storage) => ({
99
99
  setAlarm: (scheduledTime, options) => tryStoragePromise("setAlarm", () => storage.setAlarm(scheduledTime, options)),
100
100
  deleteAlarm: (options) => tryStoragePromise("deleteAlarm", () => storage.deleteAlarm(options)),
101
101
  transactionSync: (closure) => Effect.context().pipe(Effect.flatMap((context) => Effect.suspend(() => {
102
+ let aborted;
102
103
  try {
103
104
  return Effect.succeed(storage.transactionSync(() => {
104
105
  const exit = Effect.runSyncExitWith(context)(closure());
105
106
  if (Exit.isSuccess(exit)) return exit.value;
107
+ aborted = exit;
106
108
  throw exit;
107
109
  }));
108
110
  } catch (cause) {
109
- if (Exit.isExit(cause) && Exit.isFailure(cause)) return Effect.failCause(cause.cause);
111
+ if (aborted !== void 0 && cause === aborted && Exit.isFailure(aborted)) return Effect.failCause(aborted.cause);
110
112
  return Effect.fail(storageError("transactionSync", cause));
111
113
  }
112
114
  }))),
113
115
  transaction: (closure) => Effect.context().pipe(Effect.flatMap((context) => Effect.callback((resume) => {
116
+ let aborted;
114
117
  storage.transaction(async (txn) => {
115
118
  const exit = await Effect.runPromiseExitWith(context)(closure(fromDurableObjectTransaction(txn)));
116
119
  if (Exit.isSuccess(exit)) return exit.value;
120
+ aborted = exit;
117
121
  throw exit;
118
122
  }).then((value) => resume(Effect.succeed(value)), (cause) => {
119
- if (Exit.isExit(cause) && Exit.isFailure(cause)) resume(Effect.failCause(cause.cause));
123
+ if (aborted !== void 0 && cause === aborted && Exit.isFailure(aborted)) resume(Effect.failCause(aborted.cause));
120
124
  else resume(Effect.fail(storageError("transaction", cause)));
121
125
  });
122
126
  }))),
@@ -888,7 +888,8 @@ interface WorkerOptions<RRuntime, REvent = never, EventLayerError = never, Rpc e
888
888
  *
889
889
  * The layer is built inside the event's Effect scope and finalized when the
890
890
  * event effect completes. Use this for event-scoped resources such as OTLP
891
- * trace/log exporters that should flush at event completion.
891
+ * trace/log exporters. Worker fetch and RPC handlers schedule a best-effort
892
+ * flush capped at two seconds; queue handlers do not flush automatically.
892
893
  */
893
894
  readonly eventLayer?: Layer.Layer<REvent, EventLayerError, WorkerBaseContext<RRuntime>>;
894
895
  /** Main request handler. */
@@ -899,7 +900,8 @@ interface WorkerOptions<RRuntime, REvent = never, EventLayerError = never, Rpc e
899
900
  * Optional RPC methods exposed as class instance methods.
900
901
  *
901
902
  * After every invocation, the configured OTLP flusher is scheduled through
902
- * `WorkerContext.waitUntil`, including when the handler fails.
903
+ * `WorkerContext.waitUntil`, including when the handler fails. The scheduled
904
+ * flush silently settles within two seconds even if the exporter does not.
903
905
  */
904
906
  readonly rpc?: Rpc;
905
907
  }
package/dist/Vitest.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { c as WorkerEnvironment, s as WorkerEnv } from "./Binding-xJJJPmiD.mjs";
2
- import { n as DurableObjectStateService, t as DurableObjectState } from "./DurableObjectState-B3uFY1eM.mjs";
3
- import { o as WorkflowInstanceStatusName } from "./WorkflowBinding-wZ-yGHow.mjs";
2
+ import { n as DurableObjectStateService, t as DurableObjectState } from "./DurableObjectState-OupNMMUl.mjs";
3
+ import { o as WorkflowInstanceStatusName } from "./WorkflowBinding-DUZtKCVM.mjs";
4
4
  import { Effect, Layer, Schema } from "effect";
5
5
  import { DurableObject } from "cloudflare:workers";
6
6
  import { adminSecretsStore as adminSecretsStore$1, getQueueResult, introspectWorkflow as introspectWorkflow$1, introspectWorkflowInstance as introspectWorkflowInstance$1 } from "cloudflare:test";
package/dist/Vitest.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as WorkerEnvironment, i as WorkerConfig } from "./ErrorMessage-BM0K-Nik.mjs";
2
- import { r as fromDurableObjectState, t as DurableObjectState } from "./DurableObjectState-BjkK2kwH.mjs";
2
+ import { r as fromDurableObjectState, t as DurableObjectState } from "./DurableObjectState-DnnLoHV-.mjs";
3
3
  import { ConfigProvider, Effect, Exit, Layer } from "effect";
4
4
  import { env } from "cloudflare:workers";
5
5
  import { abortAllDurableObjects as abortAllDurableObjects$1, adminSecretsStore as adminSecretsStore$1, applyD1Migrations as applyD1Migrations$1, createExecutionContext, createMessageBatch, createPagesEventContext, createScheduledController, evictAllDurableObjects as evictAllDurableObjects$1, evictDurableObject as evictDurableObject$1, getQueueResult, introspectWorkflow as introspectWorkflow$1, introspectWorkflowInstance as introspectWorkflowInstance$1, listDurableObjectIds as listDurableObjectIds$1, reset as reset$1, runDurableObjectAlarm as runDurableObjectAlarm$1, runInDurableObject as runInDurableObject$1, waitOnExecutionContext } from "cloudflare:test";
@@ -1,5 +1,5 @@
1
1
  import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-xJJJPmiD.mjs";
2
- import { x as ServiceFreeSchema } from "./DurableObjectState-B3uFY1eM.mjs";
2
+ import { x as ServiceFreeSchema } from "./DurableObjectState-OupNMMUl.mjs";
3
3
  import { Context, Effect, Option, Schema } from "effect";
4
4
  declare namespace WorkflowBinding_d_exports {
5
5
  export { WorkflowBindingClient, WorkflowBindingDefinition, WorkflowInstance, WorkflowInstanceCreateBatchOptions, WorkflowInstanceCreateOptions, WorkflowInstanceEvent, WorkflowInstanceRestartOptions, WorkflowInstanceStatus, WorkflowInstanceStatusName, WorkflowOperationError, WorkflowResultDecodeError, isWorkflow, layer, makeClient };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { a as Environment_d_exports, c as WorkerEnvironment, i as Binding_d_exports, n as BindingService, o as WorkerConfig, r as BindingValidationError, s as WorkerEnv, t as BindingNotFoundError } from "./Binding-xJJJPmiD.mjs";
2
2
  import { l as Artifacts_d_exports } from "./Artifacts-DnHQA-Ax.mjs";
3
- import { C as Rpc_d_exports, D as StorageOperationError, E as DurableObjectStorage_d_exports, S as Provider, T as DurableWebSocket, _ as Definition$4, a as NativeRequest, b as RpcDefinition_d_exports, c as WorkerDefinition_d_exports, d as AsyncMethodCloudflareReturn, f as AsyncMethodKey, g as QueueBinding_d_exports, h as QueueDefinition_d_exports, i as ExecutionContext, l as ServiceBinding_d_exports, m as Queue_d_exports, o as WorkerContext, p as AsyncMethodSuccess, r as DurableObjectState_d_exports, s as Worker_d_exports, t as DurableObjectState, u as AsyncMethodArgs, v as Method$2, w as DurableObjectWebSocket_d_exports, x as ServiceFreeSchema$1, y as ReservedMethodName$2 } from "./DurableObjectState-B3uFY1eM.mjs";
3
+ import { C as Rpc_d_exports, D as StorageOperationError, E as DurableObjectStorage_d_exports, S as Provider, T as DurableWebSocket, _ as Definition$4, a as NativeRequest, b as RpcDefinition_d_exports, c as WorkerDefinition_d_exports, d as AsyncMethodCloudflareReturn, f as AsyncMethodKey, g as QueueBinding_d_exports, h as QueueDefinition_d_exports, i as ExecutionContext, l as ServiceBinding_d_exports, m as Queue_d_exports, o as WorkerContext, p as AsyncMethodSuccess, r as DurableObjectState_d_exports, s as Worker_d_exports, t as DurableObjectState, u as AsyncMethodArgs, v as Method$2, w as DurableObjectWebSocket_d_exports, x as ServiceFreeSchema$1, y as ReservedMethodName$2 } from "./DurableObjectState-OupNMMUl.mjs";
4
4
  import { t as Hyperdrive_d_exports } from "./Hyperdrive-z-9fFz15.mjs";
5
- import { a as WorkflowInstanceCreateOptions, i as WorkflowInstanceCreateBatchOptions, n as WorkflowBinding_d_exports, r as WorkflowInstance, s as WorkflowOperationError, t as WorkflowBindingClient } from "./WorkflowBinding-wZ-yGHow.mjs";
5
+ import { a as WorkflowInstanceCreateOptions, i as WorkflowInstanceCreateBatchOptions, n as WorkflowBinding_d_exports, r as WorkflowInstance, s as WorkflowOperationError, t as WorkflowBindingClient } from "./WorkflowBinding-DUZtKCVM.mjs";
6
6
  import { Cause, Config, Context, Data, DateTime, Duration, Effect, Layer, Option, Predicate, Redacted, Schema, Scope } from "effect";
7
7
  import { Headers as Headers$1, HttpClient, HttpClientResponse } from "effect/unstable/http";
8
8
  import { OtlpExporter, OtlpResource } from "effect/unstable/observability";
@@ -1111,7 +1111,9 @@ interface DurableObjectOptions<RRuntime, REvent = never, EventLayerError = never
1111
1111
  * Optional RPC methods exposed as Durable Object instance methods.
1112
1112
  *
1113
1113
  * After every invocation, the configured OTLP flusher is scheduled through
1114
- * `DurableObjectState.waitUntil`, including when the handler fails.
1114
+ * `DurableObjectState.waitUntil`, including when the handler fails. The
1115
+ * scheduled flush silently settles within two seconds even if the exporter
1116
+ * does not.
1115
1117
  */
1116
1118
  readonly rpc?: Rpc;
1117
1119
  /** Optional fetch handler for HTTP/WebSocket requests. */
@@ -1128,7 +1130,9 @@ interface DurableObjectOptions<RRuntime, REvent = never, EventLayerError = never
1128
1130
  * Optional raw alarm handler.
1129
1131
  *
1130
1132
  * After every invocation, the configured OTLP flusher is scheduled through
1131
- * `DurableObjectState.waitUntil`, including when the handler fails.
1133
+ * `DurableObjectState.waitUntil`, including when the handler fails. The
1134
+ * scheduled flush silently settles within two seconds even if the exporter
1135
+ * does not.
1132
1136
  */
1133
1137
  readonly alarm?: (alarmInfo?: globalThis.AlarmInvocationInfo) => Effect.Effect<void, unknown, HandlerContext<RRuntime | REvent>>;
1134
1138
  readonly webSocketMessage?: (socket: DurableWebSocket, message: string | ArrayBuffer) => Effect.Effect<void, unknown, HandlerContext<RRuntime | REvent>>;
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  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-BM0K-Nik.mjs";
3
3
  import { a as layer$19, i as Service$2, n as Artifacts_exports, r as Binding_exports } from "./Artifacts-rq3hUKQE.mjs";
4
- import { a as DurableObjectWebSocket_exports, i as fromExecutionContext, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState } from "./DurableObjectState-BjkK2kwH.mjs";
4
+ import { a as DurableObjectWebSocket_exports, i as fromExecutionContext, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState } from "./DurableObjectState-DnnLoHV-.mjs";
5
5
  import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Function as Function$1, Layer, ManagedRuntime, Option, Predicate, Queue, Result, Schema } from "effect";
6
6
  import { FetchHttpClient, HttpClient, HttpClientRequest, HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
7
7
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
@@ -643,16 +643,23 @@ const layer$14 = Layer.sync(CacheStorage, () => makeClient$11(caches));
643
643
  * telemetry on demand. Cloudflare isolates can freeze before the periodic
644
644
  * export interval fires. Effect-backed Worker fetch handlers, Worker or
645
645
  * Durable Object native RPC handlers, and Durable Object alarm handlers
646
- * schedule this flusher automatically.
647
- * Flush or scheduling failures do not replace the handler outcome and emit
648
- * only bounded framework diagnostics without attaching the foreign cause.
649
- * Other entrypoint lifecycles can flush explicitly or hand the flush to their
650
- * `waitUntil` mechanism:
646
+ * schedule this flusher automatically. These automatic flushes are capped at
647
+ * two seconds and silently discard timeouts and failures so telemetry cannot
648
+ * extend or fail the user event, or recursively log through an unhealthy
649
+ * exporter.
650
+ *
651
+ * Other entrypoint lifecycles, including queue handlers, do not flush
652
+ * automatically. Applications can flush explicitly or hand the flush to their
653
+ * `waitUntil` mechanism, and own the timeout and failure policy at that call
654
+ * site:
651
655
  *
652
656
  * ```ts
653
657
  * const flusher = yield* OtlpExporter.Flusher;
654
658
  *
655
- * yield* flusher.flush;
659
+ * yield* flusher.flush.pipe(
660
+ * Effect.timeoutOption("2 seconds"),
661
+ * Effect.ignoreCause,
662
+ * );
656
663
  * ```
657
664
  */
658
665
  var CloudflareOtlp_exports = /* @__PURE__ */ __exportAll({
@@ -1342,16 +1349,20 @@ function runEventPromise(...args) {
1342
1349
  }
1343
1350
  //#endregion
1344
1351
  //#region src/internal/Telemetry.ts
1345
- const logFlushFailure = Effect.logError("Telemetry flush failed").pipe(Effect.ignoreCause);
1346
- const logFlushSchedulingFailure = Effect.logError("Telemetry flush scheduling failed").pipe(Effect.ignoreCause);
1352
+ /** Maximum event lifetime spent on an internally scheduled telemetry flush. */
1353
+ const scheduledFlushTimeout = "2 seconds";
1347
1354
  /**
1348
1355
  * Schedules a configured OTLP flusher through the current Cloudflare event.
1356
+ *
1357
+ * The handed-off effect is best-effort: it settles within two seconds and
1358
+ * silently absorbs every failure cause so telemetry cannot extend or fail the
1359
+ * user event, or recursively log through an unhealthy exporter.
1349
1360
  */
1350
1361
  const scheduleTelemetryFlush$2 = (waitUntil) => Effect.gen(function* () {
1351
1362
  const flusher = yield* Effect.serviceOption(OtlpExporter.Flusher);
1352
1363
  if (Option.isNone(flusher)) return;
1353
- yield* waitUntil(flusher.value.flush.pipe(Effect.catchCause(() => logFlushFailure)));
1354
- }).pipe(Effect.catchCause(() => logFlushSchedulingFailure));
1364
+ yield* waitUntil(flusher.value.flush.pipe(Effect.timeoutOption(scheduledFlushTimeout), Effect.ignoreCause, Effect.asVoid));
1365
+ }).pipe(Effect.ignoreCause);
1355
1366
  //#endregion
1356
1367
  //#region src/Rpc.ts
1357
1368
  var Rpc_exports = /* @__PURE__ */ __exportAll({
@@ -1737,7 +1748,8 @@ const isWorkerOptions = (options) => !Effect.isEffect(options);
1737
1748
  *
1738
1749
  * The flush is handed to `ctx.waitUntil` so it never delays the response
1739
1750
  * (including streaming bodies still being consumed). It is a no-op when the
1740
- * context does not contain an `OtlpExporter.Flusher`.
1751
+ * context does not contain an `OtlpExporter.Flusher`. The handed-off effect is
1752
+ * best-effort and silently settles within two seconds.
1741
1753
  */
1742
1754
  const scheduleTelemetryFlush$1 = scheduleTelemetryFlush$2((flush) => Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flush)));
1743
1755
  function make$9(layer, optionsOrFetch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "@cloudflare/containers": "catalog:",
65
65
  "@cloudflare/vitest-pool-workers": "catalog:",
66
66
  "@cloudflare/workers-types": "catalog:",
67
- "@effect/platform-node": "^4.0.0-beta.107 <4.0.0-rc.0",
67
+ "@effect/platform-node": "^4.0.0-rc.110",
68
68
  "@effect/sql-d1": "catalog:",
69
69
  "@effect/sql-pg": "catalog:",
70
70
  "@effect/sql-sqlite-do": "catalog:",
@@ -79,10 +79,10 @@
79
79
  "@cloudflare/computer": "^0.2.0",
80
80
  "@cloudflare/vitest-pool-workers": "^0.21.3",
81
81
  "@cloudflare/workers-types": "^4.20260611.1",
82
- "@effect/sql-d1": "^4.0.0-beta.107 <4.0.0-rc.0",
83
- "@effect/sql-pg": "^4.0.0-beta.107 <4.0.0-rc.0",
84
- "@effect/sql-sqlite-do": "^4.0.0-beta.107 <4.0.0-rc.0",
85
- "effect": "^4.0.0-beta.107 <4.0.0-rc.0"
82
+ "@effect/sql-d1": "^4.0.0-rc.110",
83
+ "@effect/sql-pg": "^4.0.0-rc.110",
84
+ "@effect/sql-sqlite-do": "^4.0.0-rc.110",
85
+ "effect": "^4.0.0-rc.110"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@cloudflare/computer": {
@@ -5,16 +5,23 @@
5
5
  * telemetry on demand. Cloudflare isolates can freeze before the periodic
6
6
  * export interval fires. Effect-backed Worker fetch handlers, Worker or
7
7
  * Durable Object native RPC handlers, and Durable Object alarm handlers
8
- * schedule this flusher automatically.
9
- * Flush or scheduling failures do not replace the handler outcome and emit
10
- * only bounded framework diagnostics without attaching the foreign cause.
11
- * Other entrypoint lifecycles can flush explicitly or hand the flush to their
12
- * `waitUntil` mechanism:
8
+ * schedule this flusher automatically. These automatic flushes are capped at
9
+ * two seconds and silently discard timeouts and failures so telemetry cannot
10
+ * extend or fail the user event, or recursively log through an unhealthy
11
+ * exporter.
12
+ *
13
+ * Other entrypoint lifecycles, including queue handlers, do not flush
14
+ * automatically. Applications can flush explicitly or hand the flush to their
15
+ * `waitUntil` mechanism, and own the timeout and failure policy at that call
16
+ * site:
13
17
  *
14
18
  * ```ts
15
19
  * const flusher = yield* OtlpExporter.Flusher;
16
20
  *
17
- * yield* flusher.flush;
21
+ * yield* flusher.flush.pipe(
22
+ * Effect.timeoutOption("2 seconds"),
23
+ * Effect.ignoreCause,
24
+ * );
18
25
  * ```
19
26
  */
20
27
  import { ConfigProvider, Effect, Layer, Option } from "effect";
@@ -108,7 +108,9 @@ export interface DurableObjectOptions<
108
108
  * Optional RPC methods exposed as Durable Object instance methods.
109
109
  *
110
110
  * After every invocation, the configured OTLP flusher is scheduled through
111
- * `DurableObjectState.waitUntil`, including when the handler fails.
111
+ * `DurableObjectState.waitUntil`, including when the handler fails. The
112
+ * scheduled flush silently settles within two seconds even if the exporter
113
+ * does not.
112
114
  */
113
115
  readonly rpc?: Rpc;
114
116
  /** Optional fetch handler for HTTP/WebSocket requests. */
@@ -125,7 +127,9 @@ export interface DurableObjectOptions<
125
127
  * Optional raw alarm handler.
126
128
  *
127
129
  * After every invocation, the configured OTLP flusher is scheduled through
128
- * `DurableObjectState.waitUntil`, including when the handler fails.
130
+ * `DurableObjectState.waitUntil`, including when the handler fails. The
131
+ * scheduled flush silently settles within two seconds even if the exporter
132
+ * does not.
129
133
  */
130
134
  readonly alarm?: (
131
135
  alarmInfo?: globalThis.AlarmInvocationInfo,
@@ -196,6 +200,7 @@ export const make = <
196
200
  // SAFETY: event-layer bypass is used only for initialize; otherwise an absent layer means REvent is never.
197
201
  return Runtime.runEventPromise(
198
202
  this.runtime,
203
+ // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
199
204
  effect as Effect.Effect<A, E, HandlerContext<ROut>>,
200
205
  );
201
206
  }
@@ -320,7 +320,12 @@ export const fromDurableObjectStorage = (
320
320
  transactionSync: <A, E, R>(closure: () => Effect.Effect<A, E, R>) =>
321
321
  Effect.context<R>().pipe(
322
322
  Effect.flatMap((context) =>
323
- Effect.suspend(() => {
323
+ Effect.suspend((): Effect.Effect<A, E | StorageOperationError> => {
324
+ // Cloudflare rolls the transaction back when the callback throws, so a
325
+ // failed Exit is thrown to abort it. Keep a typed reference to that
326
+ // Exit rather than recovering its type from the untyped catch binding.
327
+ let aborted: Exit.Exit<A, E> | undefined;
328
+
324
329
  try {
325
330
  return Effect.succeed(
326
331
  storage.transactionSync(() => {
@@ -329,18 +334,14 @@ export const fromDurableObjectStorage = (
329
334
  if (Exit.isSuccess(exit)) {
330
335
  return exit.value;
331
336
  }
337
+ aborted = exit;
332
338
 
333
339
  throw exit;
334
340
  }),
335
341
  );
336
342
  } catch (cause) {
337
- if (Exit.isExit(cause) && Exit.isFailure(cause)) {
338
- // SAFETY: Exit.isFailure proves this cause carries the closure's typed E failure.
339
- return Effect.failCause(cause.cause) as Effect.Effect<
340
- A,
341
- E | StorageOperationError,
342
- R
343
- >;
343
+ if (aborted !== undefined && cause === aborted && Exit.isFailure(aborted)) {
344
+ return Effect.failCause(aborted.cause);
344
345
  }
345
346
 
346
347
  return Effect.fail(storageError("transactionSync", cause));
@@ -352,6 +353,10 @@ export const fromDurableObjectStorage = (
352
353
  Effect.context<R>().pipe(
353
354
  Effect.flatMap((context) =>
354
355
  Effect.callback<A, E | StorageOperationError>((resume) => {
356
+ // See `transactionSync`: the thrown Exit aborts Cloudflare's
357
+ // transaction, and holding it here keeps its error type.
358
+ let aborted: Exit.Exit<A, E> | undefined;
359
+
355
360
  void storage
356
361
  .transaction(async (txn) => {
357
362
  const exit = await Effect.runPromiseExitWith(context)(
@@ -361,17 +366,15 @@ export const fromDurableObjectStorage = (
361
366
  if (Exit.isSuccess(exit)) {
362
367
  return exit.value;
363
368
  }
369
+ aborted = exit;
364
370
 
365
371
  throw exit;
366
372
  })
367
373
  .then(
368
374
  (value) => resume(Effect.succeed(value)),
369
375
  (cause) => {
370
- if (Exit.isExit(cause) && Exit.isFailure(cause)) {
371
- // SAFETY: Exit.isFailure proves this cause carries the closure's typed E failure.
372
- resume(
373
- Effect.failCause(cause.cause) as Effect.Effect<A, E | StorageOperationError>,
374
- );
376
+ if (aborted !== undefined && cause === aborted && Exit.isFailure(aborted)) {
377
+ resume(Effect.failCause(aborted.cause));
375
378
  } else {
376
379
  resume(Effect.fail(storageError("transaction", cause)));
377
380
  }
package/src/Worker.ts CHANGED
@@ -185,7 +185,8 @@ export interface WorkerOptions<
185
185
  *
186
186
  * The layer is built inside the event's Effect scope and finalized when the
187
187
  * event effect completes. Use this for event-scoped resources such as OTLP
188
- * trace/log exporters that should flush at event completion.
188
+ * trace/log exporters. Worker fetch and RPC handlers schedule a best-effort
189
+ * flush capped at two seconds; queue handlers do not flush automatically.
189
190
  */
190
191
  readonly eventLayer?: Layer.Layer<REvent, EventLayerError, WorkerBaseContext<RRuntime>>;
191
192
  /** Main request handler. */
@@ -200,7 +201,8 @@ export interface WorkerOptions<
200
201
  * Optional RPC methods exposed as class instance methods.
201
202
  *
202
203
  * After every invocation, the configured OTLP flusher is scheduled through
203
- * `WorkerContext.waitUntil`, including when the handler fails.
204
+ * `WorkerContext.waitUntil`, including when the handler fails. The scheduled
205
+ * flush silently settles within two seconds even if the exporter does not.
204
206
  */
205
207
  readonly rpc?: Rpc;
206
208
  }
@@ -300,7 +302,8 @@ const isWorkerOptions = <ROut, REvent, EventLayerError, Rpc extends WorkerRpc<RO
300
302
  *
301
303
  * The flush is handed to `ctx.waitUntil` so it never delays the response
302
304
  * (including streaming bodies still being consumed). It is a no-op when the
303
- * context does not contain an `OtlpExporter.Flusher`.
305
+ * context does not contain an `OtlpExporter.Flusher`. The handed-off effect is
306
+ * best-effort and silently settles within two seconds.
304
307
  */
305
308
  const scheduleTelemetryFlush: Effect.Effect<void, never, WorkerContext> =
306
309
  Telemetry.scheduleTelemetryFlush((flush) =>
@@ -389,6 +392,7 @@ export function make<
389
392
  // SAFETY: the public make overloads permit an absent event layer only when REvent is never.
390
393
  return Runtime.runEventPromise(
391
394
  this.runtime,
395
+ // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
392
396
  effect as Effect.Effect<A, E, RuntimeContext<ROut> | Scope.Scope>,
393
397
  );
394
398
  }
package/src/Workflow.ts CHANGED
@@ -157,6 +157,7 @@ const fromWorkflowStep = (
157
157
  Effect.provideService(
158
158
  Effect.provideContext(
159
159
  // SAFETY: WorkflowStepContext is provided immediately below; context supplies every other R service.
160
+ // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
160
161
  effect as Effect.Effect<A, E, Exclude<R, WorkflowStepContext>>,
161
162
  context,
162
163
  ),
@@ -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);