effect-cf 0.25.0 → 0.25.2

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,18 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.25.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#90](https://github.com/danieljvdm/effect-cf/pull/90) [`56c1b2d`](https://github.com/danieljvdm/effect-cf/commit/56c1b2dfa30cbcc2336f28a8d8f7a48032f565df) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Flush configured OTLP telemetry after native Worker and Durable Object RPC handlers complete. Flush and scheduling failures keep the handler outcome unchanged and emit only bounded framework diagnostics.
8
+
9
+ ## 0.25.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#88](https://github.com/danieljvdm/effect-cf/pull/88) [`18941d4`](https://github.com/danieljvdm/effect-cf/commit/18941d418c296c39a36d909516eb22a74778c8e7) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Upgrade to Effect 4 beta.107 and use its explicit environment-record config provider for
14
+ Cloudflare variables.
15
+
3
16
  ## 0.25.0
4
17
 
5
18
  ### Minor Changes
@@ -59,13 +59,16 @@ declare namespace WorkerConfig {
59
59
  * Keep empty-string env values as explicit `""` config values.
60
60
  *
61
61
  * By default (absent or `false`), empty strings are treated as missing
62
- * config, matching `ConfigProvider.fromUnknown`.
62
+ * config, matching `ConfigProvider.fromEnvRecord`.
63
63
  */
64
64
  readonly preserveEmptyStrings?: boolean;
65
65
  }
66
66
  /**
67
67
  * Build a `ConfigProvider` from a Cloudflare worker `env` object.
68
68
  *
69
+ * Scalar vars and secrets use Effect's environment-record semantics;
70
+ * Cloudflare binding objects are ignored.
71
+ *
69
72
  * Empty-string values are treated as missing config by default; pass
70
73
  * `{ preserveEmptyStrings: true }` to keep them as explicit values.
71
74
  */
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { Cause, Config, ConfigProvider, Context, Data, Effect, Exit, Option, Result, Schema } from "effect";
2
+ import { Cause, Config, ConfigProvider, Context, Data, Effect, Exit, Option, Predicate, Result, Schema } from "effect";
3
3
  //#region src/Environment.ts
4
4
  var Environment_exports = /* @__PURE__ */ __exportAll({
5
5
  WorkerConfig: () => WorkerConfig,
@@ -16,7 +16,11 @@ let WorkerConfig;
16
16
  _WorkerConfig.number = (name) => Config.number(name);
17
17
  _WorkerConfig.integer = (name) => Config.int(name);
18
18
  _WorkerConfig.boolean = (name) => Config.boolean(name);
19
- const providerFromEnv = _WorkerConfig.providerFromEnv = (env, options) => ConfigProvider.fromUnknown(env, options);
19
+ const providerFromEnv = _WorkerConfig.providerFromEnv = (env, options) => {
20
+ const record = {};
21
+ for (const [key, value] of Object.entries(env)) if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) record[key] = String(value);
22
+ return ConfigProvider.fromEnvRecord(record, options);
23
+ };
20
24
  const providerWith = _WorkerConfig.providerWith = (makeProvider) => Effect.map(WorkerEnvironment, makeProvider);
21
25
  const provider = _WorkerConfig.provider = providerWith(providerFromEnv);
22
26
  _WorkerConfig.providerLayer = ConfigProvider.layer(provider);
@@ -1,4 +1,4 @@
1
- import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-DI7u3fEY.mjs";
1
+ import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-COCtoJXa.mjs";
2
2
  import { Context, Effect, Layer } from "effect";
3
3
  declare namespace Hyperdrive_d_exports {
4
4
  export { HyperdriveClient, HyperdriveDefinition, HyperdriveService, LayerOptions, Tag, TagClass, isHyperdrive, layer, make, makeClient };
@@ -1,5 +1,5 @@
1
- import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-DI7u3fEY.mjs";
2
- import { n as LayerOptions, r as TagClass } from "./Hyperdrive-BwYp8Rsz.mjs";
1
+ import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-COCtoJXa.mjs";
2
+ import { n as LayerOptions, r as TagClass } from "./Hyperdrive-6p59yyVk.mjs";
3
3
  import { Layer } from "effect";
4
4
  import { PgClient } from "@effect/sql-pg";
5
5
  import { SqlClient, SqlError } from "effect/unstable/sql";
package/dist/Vitest.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as WorkerEnvironment, s as WorkerEnv } from "./Binding-DI7u3fEY.mjs";
2
- import { c as DurableObjectState, l as DurableObjectStateService, o as WorkflowInstanceStatusName } from "./WorkflowBinding--eMVaixT.mjs";
1
+ import { c as WorkerEnvironment, s as WorkerEnv } from "./Binding-COCtoJXa.mjs";
2
+ import { c as DurableObjectState, l as DurableObjectStateService, o as WorkflowInstanceStatusName } from "./WorkflowBinding-D5iWzCUa.mjs";
3
3
  import { Effect, Layer } from "effect";
4
4
  import { DurableObject } from "cloudflare:workers";
5
5
  import { adminSecretsStore as adminSecretsStore$1, getQueueResult, introspectWorkflow as introspectWorkflow$1, introspectWorkflowInstance as introspectWorkflowInstance$1 } from "cloudflare:test";
package/dist/Vitest.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as WorkerConfig, f as WorkerEnvironment, r as fromDurableObjectState, t as DurableObjectState } from "./DurableObjectState-B5RCFnRy.mjs";
1
+ import { d as WorkerConfig, f as WorkerEnvironment, r as fromDurableObjectState, t as DurableObjectState } from "./DurableObjectState-CEh0DVw7.mjs";
2
2
  import { ConfigProvider, Effect, Exit, Layer } from "effect";
3
3
  import { env } from "cloudflare:workers";
4
4
  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,4 +1,4 @@
1
- import { c as WorkerEnvironment, n as BindingService, r as BindingValidationError, s as WorkerEnv, t as BindingNotFoundError } from "./Binding-DI7u3fEY.mjs";
1
+ import { c as WorkerEnvironment, n as BindingService, r as BindingValidationError, s as WorkerEnv, t as BindingNotFoundError } from "./Binding-COCtoJXa.mjs";
2
2
  import { Cause, Context, Effect, Layer, Option, Schema, Scope } from "effect";
3
3
  import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
4
4
  import { RpcStub, RpcTarget, WorkerEntrypoint } from "cloudflare:workers";
@@ -881,7 +881,12 @@ interface WorkerOptions<RRuntime, REvent = never, EventLayerError = never, Rpc e
881
881
  readonly fetch?: Effect.Effect<WorkerFetchSuccess, unknown, WorkerFetchContext<RRuntime | REvent>>;
882
882
  /** Queue consumer handler invoked per message batch. */
883
883
  readonly queue?: QueueHandler<RRuntime | REvent>;
884
- /** Optional RPC methods exposed as class instance methods. */
884
+ /**
885
+ * Optional RPC methods exposed as class instance methods.
886
+ *
887
+ * After every invocation, the configured OTLP flusher is scheduled through
888
+ * `WorkerContext.waitUntil`, including when the handler fails.
889
+ */
885
890
  readonly rpc?: Rpc;
886
891
  }
887
892
  type FetchWorkerOptions<RRuntime, REvent = never, EventLayerError = never> = Omit<WorkerOptions<RRuntime, REvent, EventLayerError, Record<never, never>>, "rpc"> & {
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
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-DI7u3fEY.mjs";
2
- import { t as Hyperdrive_d_exports } from "./Hyperdrive-BwYp8Rsz.mjs";
3
- import { A as Rpc_d_exports, C as QueueBinding_d_exports, D as RpcDefinition_d_exports, E as ReservedMethodName$2, M as DurableWebSocket, N as DurableObjectStorage_d_exports, O as ServiceFreeSchema$1, P as StorageOperationError, S as QueueDefinition_d_exports, T as Method$2, _ as AsyncMethodArgs, a as WorkflowInstanceCreateOptions, b as AsyncMethodSuccess, c as DurableObjectState, d as ExecutionContext, f as NativeRequest, g as ServiceBinding_d_exports, h as WorkerDefinition_d_exports, i as WorkflowInstanceCreateBatchOptions, j as DurableObjectWebSocket_d_exports, k as Provider, m as Worker_d_exports, n as WorkflowBinding_d_exports, p as WorkerContext, r as WorkflowInstance, s as WorkflowOperationError, t as WorkflowBindingClient, u as DurableObjectState_d_exports, v as AsyncMethodCloudflareReturn, w as Definition$4, x as Queue_d_exports, y as AsyncMethodKey } from "./WorkflowBinding--eMVaixT.mjs";
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-COCtoJXa.mjs";
2
+ import { t as Hyperdrive_d_exports } from "./Hyperdrive-6p59yyVk.mjs";
3
+ import { A as Rpc_d_exports, C as QueueBinding_d_exports, D as RpcDefinition_d_exports, E as ReservedMethodName$2, M as DurableWebSocket, N as DurableObjectStorage_d_exports, O as ServiceFreeSchema$1, P as StorageOperationError, S as QueueDefinition_d_exports, T as Method$2, _ as AsyncMethodArgs, a as WorkflowInstanceCreateOptions, b as AsyncMethodSuccess, c as DurableObjectState, d as ExecutionContext, f as NativeRequest, g as ServiceBinding_d_exports, h as WorkerDefinition_d_exports, i as WorkflowInstanceCreateBatchOptions, j as DurableObjectWebSocket_d_exports, k as Provider, m as Worker_d_exports, n as WorkflowBinding_d_exports, p as WorkerContext, r as WorkflowInstance, s as WorkflowOperationError, t as WorkflowBindingClient, u as DurableObjectState_d_exports, v as AsyncMethodCloudflareReturn, w as Definition$4, x as Queue_d_exports, y as AsyncMethodKey } from "./WorkflowBinding-D5iWzCUa.mjs";
4
4
  import { Config, Context, Data, DateTime, Duration, Effect, Layer, Option, Redacted, Schema, Scope } from "effect";
5
5
  import { Headers as Headers$1, HttpClient, HttpClientResponse } from "effect/unstable/http";
6
6
  import { OtlpExporter } from "effect/unstable/observability";
@@ -1099,7 +1099,12 @@ interface DurableObjectOptions<RRuntime, REvent = never, EventLayerError = never
1099
1099
  * Object storage if work must happen only once per id.
1100
1100
  */
1101
1101
  readonly initialize?: Effect.Effect<void, unknown, HandlerContext<RRuntime>>;
1102
- /** Optional RPC methods exposed as Durable Object instance methods. */
1102
+ /**
1103
+ * Optional RPC methods exposed as Durable Object instance methods.
1104
+ *
1105
+ * After every invocation, the configured OTLP flusher is scheduled through
1106
+ * `DurableObjectState.waitUntil`, including when the handler fails.
1107
+ */
1103
1108
  readonly rpc?: Rpc;
1104
1109
  /** Optional fetch handler for HTTP/WebSocket requests. */
1105
1110
  readonly fetch?: Effect.Effect<Response, unknown, FetchContext<RRuntime | REvent>>;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { a as DurableObjectWebSocket_exports, c as causeMessage, d as WorkerConfig, f as WorkerEnvironment, i as fromExecutionContext, l as violationsMessage, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState, u as Environment_exports } from "./DurableObjectState-B5RCFnRy.mjs";
2
+ import { a as DurableObjectWebSocket_exports, c as causeMessage, d as WorkerConfig, f as WorkerEnvironment, i as fromExecutionContext, l as violationsMessage, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState, u as Environment_exports } from "./DurableObjectState-CEh0DVw7.mjs";
3
3
  import { Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Function as Function$1, Layer, ManagedRuntime, Option, Predicate, Queue, Result, Schema } from "effect";
4
4
  import { FetchHttpClient, HttpClient, HttpClientRequest, HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
5
5
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
@@ -683,8 +683,12 @@ const layer$14 = Layer.sync(CacheStorage, () => makeClient$11(caches));
683
683
  *
684
684
  * Every layer exposes {@link OtlpExporter.Flusher}, which drains buffered
685
685
  * telemetry on demand. Cloudflare isolates can freeze before the periodic
686
- * export interval fires, so flush explicitly before returning a response, or
687
- * hand the flush to `ctx.waitUntil`:
686
+ * export interval fires. Effect-backed Worker fetch handlers and Worker or
687
+ * Durable Object native RPC handlers schedule this flusher automatically.
688
+ * Flush or scheduling failures do not replace the handler outcome and emit
689
+ * only bounded framework diagnostics without attaching the foreign cause.
690
+ * Other entrypoint lifecycles can flush explicitly or hand the flush to their
691
+ * `waitUntil` mechanism:
688
692
  *
689
693
  * ```ts
690
694
  * const flusher = yield* OtlpExporter.Flusher;
@@ -1347,13 +1351,14 @@ const make$11 = (id, methods) => {
1347
1351
  //#endregion
1348
1352
  //#region src/internal/Entrypoint.ts
1349
1353
  const provideEntrypointServices = (layer, services) => layer.pipe(Layer.provideMerge(services));
1350
- const defineEntrypointRpcMethods = (target, prototype, rpc, reservedMethodNames, run) => {
1354
+ const defineEntrypointRpcMethods = (target, prototype, rpc, reservedMethodNames, run, onExit) => {
1351
1355
  const methods = rpc ?? {};
1352
1356
  assertNoReservedMethods(target, methods, reservedMethodNames);
1353
1357
  for (const [key, method] of Object.entries(methods)) Object.defineProperty(prototype, key, {
1354
1358
  enumerable: true,
1355
1359
  value(...args) {
1356
- return run(this, Effect.suspend(() => method(...args)).pipe(Effect.mapError(encodeWireError)));
1360
+ const handler = Effect.suspend(() => method(...args)).pipe(Effect.mapError(encodeWireError));
1361
+ return run(this, onExit === void 0 ? handler : handler.pipe(Effect.onExit(() => onExit(this))));
1357
1362
  }
1358
1363
  });
1359
1364
  };
@@ -1386,6 +1391,18 @@ const runEventPromise = (runtime, effect, eventLayer) => {
1386
1391
  return runtime.runPromise(Effect.scoped(withEventLayer));
1387
1392
  };
1388
1393
  //#endregion
1394
+ //#region src/internal/Telemetry.ts
1395
+ const logFlushFailure = Effect.logError("Telemetry flush failed").pipe(Effect.ignoreCause);
1396
+ const logFlushSchedulingFailure = Effect.logError("Telemetry flush scheduling failed").pipe(Effect.ignoreCause);
1397
+ /**
1398
+ * Schedules a configured OTLP flusher through the current Cloudflare event.
1399
+ */
1400
+ const scheduleTelemetryFlush$2 = (waitUntil) => Effect.gen(function* () {
1401
+ const flusher = yield* Effect.serviceOption(OtlpExporter.Flusher);
1402
+ if (Option.isNone(flusher)) return;
1403
+ yield* waitUntil(flusher.value.flush.pipe(Effect.catchCause(() => logFlushFailure)));
1404
+ }).pipe(Effect.catchCause(() => logFlushSchedulingFailure));
1405
+ //#endregion
1389
1406
  //#region src/Rpc.ts
1390
1407
  var Rpc_exports = /* @__PURE__ */ __exportAll({
1391
1408
  RpcStub: () => RpcStub,
@@ -1755,10 +1772,7 @@ const isWorkerOptions = (options) => !Effect.isEffect(options);
1755
1772
  * (including streaming bodies still being consumed). It is a no-op when the
1756
1773
  * context does not contain an `OtlpExporter.Flusher`.
1757
1774
  */
1758
- const scheduleTelemetryFlush = Effect.flatMap(Effect.serviceOption(OtlpExporter.Flusher), Option.match({
1759
- onNone: () => Effect.void,
1760
- onSome: (flusher) => Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flusher.flush))
1761
- }));
1775
+ const scheduleTelemetryFlush$1 = scheduleTelemetryFlush$2((flush) => Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flush)));
1762
1776
  function make$9(layer, optionsOrFetch) {
1763
1777
  const options = isWorkerOptions(optionsOrFetch) ? optionsOrFetch : { fetch: optionsOrFetch };
1764
1778
  class EffectWorker extends WorkerEntrypoint {
@@ -1778,7 +1792,7 @@ function make$9(layer, optionsOrFetch) {
1778
1792
  if (fetchHandler === void 0) return Promise.resolve(new Response("Not Found", { status: 404 }));
1779
1793
  const ctx = requestContext ?? this.ctx;
1780
1794
  const requestServices = Layer.mergeAll(Layer.succeed(NativeRequest, request), Layer.succeed(HttpServerRequest.HttpServerRequest, HttpServerRequest.fromWeb(request)), Layer.succeed(ExecutionContext, ctx), Layer.succeed(WorkerContext, fromExecutionContext(ctx)));
1781
- return this[RunSymbol$1](renderFetchSuccess(fetchHandler).pipe(Effect.tap(() => scheduleTelemetryFlush), Effect.provide(requestServices)));
1795
+ return this[RunSymbol$1](renderFetchSuccess(fetchHandler).pipe(Effect.tap(() => scheduleTelemetryFlush$1), Effect.provide(requestServices)));
1782
1796
  }
1783
1797
  queue(batch) {
1784
1798
  const queueHandler = options.queue;
@@ -1787,7 +1801,7 @@ function make$9(layer, optionsOrFetch) {
1787
1801
  return this[RunSymbol$1](queueHandler(fromMessageBatch(batch, messages)));
1788
1802
  }
1789
1803
  }
1790
- defineEntrypointRpcMethods("Worker", EffectWorker.prototype, options.rpc, reservedMethodNames$2, (self, effect) => self[RunSymbol$1](effect));
1804
+ defineEntrypointRpcMethods("Worker", EffectWorker.prototype, options.rpc, reservedMethodNames$2, (self, effect) => self[RunSymbol$1](effect), () => scheduleTelemetryFlush$1);
1791
1805
  return assumeEntrypointClass(EffectWorker);
1792
1806
  }
1793
1807
  /**
@@ -2080,6 +2094,7 @@ const reservedMethodNames = /* @__PURE__ */ new Set([
2080
2094
  "webSocketError"
2081
2095
  ]);
2082
2096
  const RunSymbol = Symbol.for("effect-cf/DurableObject/run");
2097
+ const scheduleTelemetryFlush = scheduleTelemetryFlush$2((flush) => Effect.flatMap(DurableObjectState, (state) => state.waitUntil(flush)));
2083
2098
  /**
2084
2099
  * Creates a Durable Object class backed by a single managed Effect runtime.
2085
2100
  */
@@ -2120,7 +2135,7 @@ const make$7 = (layer, options = {}) => {
2120
2135
  if (options.webSocketError !== void 0) return this[RunSymbol](options.webSocketError(fromWebSocket(socket), error));
2121
2136
  }
2122
2137
  }
2123
- defineEntrypointRpcMethods("Durable Object", EffectDurableObject.prototype, options.rpc, reservedMethodNames, (self, effect) => self[RunSymbol](effect));
2138
+ defineEntrypointRpcMethods("Durable Object", EffectDurableObject.prototype, options.rpc, reservedMethodNames, (self, effect) => self[RunSymbol](effect), () => scheduleTelemetryFlush);
2124
2139
  return assumeEntrypointClass(EffectDurableObject);
2125
2140
  };
2126
2141
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,7 +48,7 @@
48
48
  "@cloudflare/containers": "catalog:",
49
49
  "@cloudflare/vitest-pool-workers": "catalog:",
50
50
  "@cloudflare/workers-types": "catalog:",
51
- "@effect/platform-node": "^4.0.0-beta.105",
51
+ "@effect/platform-node": "^4.0.0-beta.107 <4.0.0-rc.0",
52
52
  "@effect/sql-d1": "catalog:",
53
53
  "@effect/sql-pg": "catalog:",
54
54
  "@effect/sql-sqlite-do": "catalog:",
@@ -60,10 +60,10 @@
60
60
  "peerDependencies": {
61
61
  "@cloudflare/vitest-pool-workers": "^0.21.3",
62
62
  "@cloudflare/workers-types": "^4.20260611.1",
63
- "@effect/sql-d1": "^4.0.0-beta.105",
64
- "@effect/sql-pg": "^4.0.0-beta.105",
65
- "@effect/sql-sqlite-do": "^4.0.0-beta.105",
66
- "effect": "^4.0.0-beta.105"
63
+ "@effect/sql-d1": "^4.0.0-beta.107 <4.0.0-rc.0",
64
+ "@effect/sql-pg": "^4.0.0-beta.107 <4.0.0-rc.0",
65
+ "@effect/sql-sqlite-do": "^4.0.0-beta.107 <4.0.0-rc.0",
66
+ "effect": "^4.0.0-beta.107 <4.0.0-rc.0"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "@cloudflare/vitest-pool-workers": {
@@ -3,8 +3,12 @@
3
3
  *
4
4
  * Every layer exposes {@link OtlpExporter.Flusher}, which drains buffered
5
5
  * telemetry on demand. Cloudflare isolates can freeze before the periodic
6
- * export interval fires, so flush explicitly before returning a response, or
7
- * hand the flush to `ctx.waitUntil`:
6
+ * export interval fires. Effect-backed Worker fetch handlers and Worker or
7
+ * Durable Object native RPC handlers schedule this flusher automatically.
8
+ * Flush or scheduling failures do not replace the handler outcome and emit
9
+ * only bounded framework diagnostics without attaching the foreign cause.
10
+ * Other entrypoint lifecycles can flush explicitly or hand the flush to their
11
+ * `waitUntil` mechanism:
8
12
  *
9
13
  * ```ts
10
14
  * const flusher = yield* OtlpExporter.Flusher;
@@ -7,6 +7,7 @@ import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState
7
7
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
8
8
  import * as Entrypoint from "./internal/Entrypoint";
9
9
  import * as Runtime from "./internal/Runtime";
10
+ import * as Telemetry from "./internal/Telemetry";
10
11
 
11
12
  const reservedMethodNames = new Set<string>([
12
13
  "constructor",
@@ -29,6 +30,11 @@ type RunOptions = {
29
30
 
30
31
  const RunSymbol = Symbol.for("effect-cf/DurableObject/run");
31
32
 
33
+ const scheduleTelemetryFlush: Effect.Effect<void, never, DurableObjectState> =
34
+ Telemetry.scheduleTelemetryFlush((flush) =>
35
+ Effect.flatMap(DurableObjectState, (state) => state.waitUntil(flush)),
36
+ );
37
+
32
38
  /**
33
39
  * Effect type for Durable Object lifecycle and RPC handlers.
34
40
  */
@@ -98,7 +104,12 @@ export interface DurableObjectOptions<
98
104
  * Object storage if work must happen only once per id.
99
105
  */
100
106
  readonly initialize?: Effect.Effect<void, unknown, HandlerContext<RRuntime>>;
101
- /** Optional RPC methods exposed as Durable Object instance methods. */
107
+ /**
108
+ * Optional RPC methods exposed as Durable Object instance methods.
109
+ *
110
+ * After every invocation, the configured OTLP flusher is scheduled through
111
+ * `DurableObjectState.waitUntil`, including when the handler fails.
112
+ */
102
113
  readonly rpc?: Rpc;
103
114
  /** Optional fetch handler for HTTP/WebSocket requests. */
104
115
  readonly fetch?: Effect.Effect<Response, unknown, FetchContext<RRuntime | REvent>>;
@@ -244,6 +255,7 @@ export const make = <
244
255
  options.rpc,
245
256
  reservedMethodNames,
246
257
  (self, effect) => self[RunSymbol](effect),
258
+ () => scheduleTelemetryFlush,
247
259
  );
248
260
 
249
261
  return Entrypoint.assumeEntrypointClass<DurableObjectClass<Rpc, ROut | REvent>>(
@@ -1,4 +1,4 @@
1
- import { Config, ConfigProvider, Context, Effect, type Layer } from "effect";
1
+ import { Config, ConfigProvider, Context, Effect, type Layer, Predicate } from "effect";
2
2
 
3
3
  /** Cloudflare worker environment object (`env`). */
4
4
  export type WorkerEnv = Cloudflare.Env;
@@ -78,7 +78,7 @@ export namespace WorkerConfig {
78
78
  * Keep empty-string env values as explicit `""` config values.
79
79
  *
80
80
  * By default (absent or `false`), empty strings are treated as missing
81
- * config, matching `ConfigProvider.fromUnknown`.
81
+ * config, matching `ConfigProvider.fromEnvRecord`.
82
82
  */
83
83
  readonly preserveEmptyStrings?: boolean;
84
84
  }
@@ -86,11 +86,23 @@ export namespace WorkerConfig {
86
86
  /**
87
87
  * Build a `ConfigProvider` from a Cloudflare worker `env` object.
88
88
  *
89
+ * Scalar vars and secrets use Effect's environment-record semantics;
90
+ * Cloudflare binding objects are ignored.
91
+ *
89
92
  * Empty-string values are treated as missing config by default; pass
90
93
  * `{ preserveEmptyStrings: true }` to keep them as explicit values.
91
94
  */
92
- export const providerFromEnv = (env: WorkerEnv, options?: ProviderOptions) =>
93
- ConfigProvider.fromUnknown(env, options);
95
+ export const providerFromEnv = (env: WorkerEnv, options?: ProviderOptions) => {
96
+ const record: Record<string, string> = {};
97
+
98
+ for (const [key, value] of Object.entries(env)) {
99
+ if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) {
100
+ record[key] = String(value);
101
+ }
102
+ }
103
+
104
+ return ConfigProvider.fromEnvRecord(record, options);
105
+ };
94
106
 
95
107
  /**
96
108
  * Build a `ConfigProvider` from the current `WorkerEnvironment` with a custom
package/src/Worker.ts CHANGED
@@ -1,21 +1,13 @@
1
1
  import { WorkerEntrypoint as CloudflareWorkerEntrypoint } from "cloudflare:workers";
2
- import {
3
- type Cause,
4
- Context,
5
- Effect,
6
- Layer,
7
- type ManagedRuntime,
8
- Option,
9
- type Scope,
10
- } from "effect";
2
+ import { type Cause, Context, Effect, Layer, type ManagedRuntime, type Scope } from "effect";
11
3
  import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
12
- import { OtlpExporter } from "effect/unstable/observability";
13
4
 
14
5
  import { WorkerEnvironment, type WorkerEnv } from "./Environment";
15
6
  import { fromMessage, fromMessageBatch, type QueueHandler } from "./Queue";
16
7
  import type * as RpcDefinition from "./RpcDefinition";
17
8
  import * as Entrypoint from "./internal/Entrypoint";
18
9
  import * as Runtime from "./internal/Runtime";
10
+ import * as Telemetry from "./internal/Telemetry";
19
11
  import { fromExecutionContext } from "./internal/WorkerContext";
20
12
 
21
13
  /**
@@ -208,7 +200,12 @@ export interface WorkerOptions<
208
200
  >;
209
201
  /** Queue consumer handler invoked per message batch. */
210
202
  readonly queue?: QueueHandler<RRuntime | REvent>;
211
- /** Optional RPC methods exposed as class instance methods. */
203
+ /**
204
+ * Optional RPC methods exposed as class instance methods.
205
+ *
206
+ * After every invocation, the configured OTLP flusher is scheduled through
207
+ * `WorkerContext.waitUntil`, including when the handler fails.
208
+ */
212
209
  readonly rpc?: Rpc;
213
210
  }
214
211
 
@@ -292,14 +289,10 @@ const isWorkerOptions = <ROut, REvent, EventLayerError, Rpc extends WorkerRpc<RO
292
289
  * (including streaming bodies still being consumed). It is a no-op when the
293
290
  * context does not contain an `OtlpExporter.Flusher`.
294
291
  */
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
- );
292
+ const scheduleTelemetryFlush: Effect.Effect<void, never, WorkerContext> =
293
+ Telemetry.scheduleTelemetryFlush((flush) =>
294
+ Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flush)),
295
+ );
303
296
 
304
297
  /**
305
298
  * Creates a Cloudflare worker class backed by a single managed Effect runtime.
@@ -424,6 +417,7 @@ export function make<
424
417
  options.rpc,
425
418
  reservedMethodNames,
426
419
  (self, effect) => self[RunSymbol](effect),
420
+ () => scheduleTelemetryFlush,
427
421
  );
428
422
 
429
423
  return Entrypoint.assumeEntrypointClass<WorkerClass<Rpc, ROut | REvent>>(EffectWorker);
@@ -19,6 +19,7 @@ export const defineEntrypointRpcMethods = <Self>(
19
19
  rpc: EntrypointRpc | undefined,
20
20
  reservedMethodNames: ReadonlySet<string>,
21
21
  run: (self: Self, effect: Effect.Effect<any, any, any>) => Promise<unknown>,
22
+ onExit?: (self: Self) => Effect.Effect<void, never, any>,
22
23
  ): void => {
23
24
  const methods = rpc ?? {};
24
25
 
@@ -28,11 +29,13 @@ export const defineEntrypointRpcMethods = <Self>(
28
29
  Object.defineProperty(prototype, key, {
29
30
  enumerable: true,
30
31
  value(this: Self, ...args: AnyArgs) {
32
+ const handler = Effect.suspend(() => method(...args)).pipe(
33
+ Effect.mapError(RpcDefinition.encodeWireError),
34
+ );
35
+
31
36
  return run(
32
37
  this,
33
- Effect.suspend(() => method(...args)).pipe(
34
- Effect.mapError(RpcDefinition.encodeWireError),
35
- ),
38
+ onExit === undefined ? handler : handler.pipe(Effect.onExit(() => onExit(this))),
36
39
  );
37
40
  },
38
41
  });
@@ -0,0 +1,27 @@
1
+ import { Effect, Option } from "effect";
2
+ import { OtlpExporter } from "effect/unstable/observability";
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
+ );
9
+
10
+ /**
11
+ * Schedules a configured OTLP flusher through the current Cloudflare event.
12
+ */
13
+ export const scheduleTelemetryFlush = <R>(
14
+ waitUntil: (flush: Effect.Effect<void>) => Effect.Effect<void, never, R>,
15
+ ): Effect.Effect<void, never, R> =>
16
+ Effect.gen(function* () {
17
+ const flusher = yield* Effect.serviceOption(OtlpExporter.Flusher);
18
+
19
+ if (Option.isNone(flusher)) {
20
+ return;
21
+ }
22
+
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));