effect-cf 0.25.1 → 0.25.3

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,17 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.25.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#92](https://github.com/danieljvdm/effect-cf/pull/92) [`f174800`](https://github.com/danieljvdm/effect-cf/commit/f1748004e463d4e54c22e7c77fdea68ff8de7f82) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Flush configured OTLP telemetry after Durable Object alarm handlers, including failed alarms, so buffered telemetry is exported consistently with fetch and native RPC handlers.
8
+
9
+ ## 0.25.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#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.
14
+
3
15
  ## 0.25.1
4
16
 
5
17
  ### Patch Changes
package/dist/Vitest.d.mts CHANGED
@@ -1,5 +1,5 @@
1
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-Bzs5kmoE.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";
@@ -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
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
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-Bzs5kmoE.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>>;
@@ -1111,6 +1116,12 @@ interface DurableObjectOptions<RRuntime, REvent = never, EventLayerError = never
1111
1116
  * the Durable Object's single managed runtime boundary.
1112
1117
  */
1113
1118
  readonly alarms?: Effect.Effect<unknown, unknown, HandlerContext<RRuntime | REvent>>;
1119
+ /**
1120
+ * Optional raw alarm handler.
1121
+ *
1122
+ * After every invocation, the configured OTLP flusher is scheduled through
1123
+ * `DurableObjectState.waitUntil`, including when the handler fails.
1124
+ */
1114
1125
  readonly alarm?: (alarmInfo?: globalThis.AlarmInvocationInfo) => Effect.Effect<void, unknown, HandlerContext<RRuntime | REvent>>;
1115
1126
  readonly webSocketMessage?: (socket: DurableWebSocket, message: string | ArrayBuffer) => Effect.Effect<void, unknown, HandlerContext<RRuntime | REvent>>;
1116
1127
  readonly webSocketClose?: (socket: DurableWebSocket, code: number, reason: string, wasClean: boolean) => Effect.Effect<void, unknown, HandlerContext<RRuntime | REvent>>;
package/dist/index.mjs CHANGED
@@ -683,8 +683,13 @@ 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, Worker or
687
+ * Durable Object native RPC handlers, and Durable Object alarm handlers
688
+ * schedule this flusher automatically.
689
+ * Flush or scheduling failures do not replace the handler outcome and emit
690
+ * only bounded framework diagnostics without attaching the foreign cause.
691
+ * Other entrypoint lifecycles can flush explicitly or hand the flush to their
692
+ * `waitUntil` mechanism:
688
693
  *
689
694
  * ```ts
690
695
  * const flusher = yield* OtlpExporter.Flusher;
@@ -1347,13 +1352,14 @@ const make$11 = (id, methods) => {
1347
1352
  //#endregion
1348
1353
  //#region src/internal/Entrypoint.ts
1349
1354
  const provideEntrypointServices = (layer, services) => layer.pipe(Layer.provideMerge(services));
1350
- const defineEntrypointRpcMethods = (target, prototype, rpc, reservedMethodNames, run) => {
1355
+ const defineEntrypointRpcMethods = (target, prototype, rpc, reservedMethodNames, run, onExit) => {
1351
1356
  const methods = rpc ?? {};
1352
1357
  assertNoReservedMethods(target, methods, reservedMethodNames);
1353
1358
  for (const [key, method] of Object.entries(methods)) Object.defineProperty(prototype, key, {
1354
1359
  enumerable: true,
1355
1360
  value(...args) {
1356
- return run(this, Effect.suspend(() => method(...args)).pipe(Effect.mapError(encodeWireError)));
1361
+ const handler = Effect.suspend(() => method(...args)).pipe(Effect.mapError(encodeWireError));
1362
+ return run(this, onExit === void 0 ? handler : handler.pipe(Effect.onExit(() => onExit(this))));
1357
1363
  }
1358
1364
  });
1359
1365
  };
@@ -1386,6 +1392,18 @@ const runEventPromise = (runtime, effect, eventLayer) => {
1386
1392
  return runtime.runPromise(Effect.scoped(withEventLayer));
1387
1393
  };
1388
1394
  //#endregion
1395
+ //#region src/internal/Telemetry.ts
1396
+ const logFlushFailure = Effect.logError("Telemetry flush failed").pipe(Effect.ignoreCause);
1397
+ const logFlushSchedulingFailure = Effect.logError("Telemetry flush scheduling failed").pipe(Effect.ignoreCause);
1398
+ /**
1399
+ * Schedules a configured OTLP flusher through the current Cloudflare event.
1400
+ */
1401
+ const scheduleTelemetryFlush$2 = (waitUntil) => Effect.gen(function* () {
1402
+ const flusher = yield* Effect.serviceOption(OtlpExporter.Flusher);
1403
+ if (Option.isNone(flusher)) return;
1404
+ yield* waitUntil(flusher.value.flush.pipe(Effect.catchCause(() => logFlushFailure)));
1405
+ }).pipe(Effect.catchCause(() => logFlushSchedulingFailure));
1406
+ //#endregion
1389
1407
  //#region src/Rpc.ts
1390
1408
  var Rpc_exports = /* @__PURE__ */ __exportAll({
1391
1409
  RpcStub: () => RpcStub,
@@ -1755,10 +1773,7 @@ const isWorkerOptions = (options) => !Effect.isEffect(options);
1755
1773
  * (including streaming bodies still being consumed). It is a no-op when the
1756
1774
  * context does not contain an `OtlpExporter.Flusher`.
1757
1775
  */
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
- }));
1776
+ const scheduleTelemetryFlush$1 = scheduleTelemetryFlush$2((flush) => Effect.flatMap(WorkerContext, (workerContext) => workerContext.waitUntil(flush)));
1762
1777
  function make$9(layer, optionsOrFetch) {
1763
1778
  const options = isWorkerOptions(optionsOrFetch) ? optionsOrFetch : { fetch: optionsOrFetch };
1764
1779
  class EffectWorker extends WorkerEntrypoint {
@@ -1778,7 +1793,7 @@ function make$9(layer, optionsOrFetch) {
1778
1793
  if (fetchHandler === void 0) return Promise.resolve(new Response("Not Found", { status: 404 }));
1779
1794
  const ctx = requestContext ?? this.ctx;
1780
1795
  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)));
1796
+ return this[RunSymbol$1](renderFetchSuccess(fetchHandler).pipe(Effect.tap(() => scheduleTelemetryFlush$1), Effect.provide(requestServices)));
1782
1797
  }
1783
1798
  queue(batch) {
1784
1799
  const queueHandler = options.queue;
@@ -1787,7 +1802,7 @@ function make$9(layer, optionsOrFetch) {
1787
1802
  return this[RunSymbol$1](queueHandler(fromMessageBatch(batch, messages)));
1788
1803
  }
1789
1804
  }
1790
- defineEntrypointRpcMethods("Worker", EffectWorker.prototype, options.rpc, reservedMethodNames$2, (self, effect) => self[RunSymbol$1](effect));
1805
+ defineEntrypointRpcMethods("Worker", EffectWorker.prototype, options.rpc, reservedMethodNames$2, (self, effect) => self[RunSymbol$1](effect), () => scheduleTelemetryFlush$1);
1791
1806
  return assumeEntrypointClass(EffectWorker);
1792
1807
  }
1793
1808
  /**
@@ -2080,6 +2095,7 @@ const reservedMethodNames = /* @__PURE__ */ new Set([
2080
2095
  "webSocketError"
2081
2096
  ]);
2082
2097
  const RunSymbol = Symbol.for("effect-cf/DurableObject/run");
2098
+ const scheduleTelemetryFlush = scheduleTelemetryFlush$2((flush) => Effect.flatMap(DurableObjectState, (state) => state.waitUntil(flush)));
2083
2099
  /**
2084
2100
  * Creates a Durable Object class backed by a single managed Effect runtime.
2085
2101
  */
@@ -2103,12 +2119,11 @@ const make$7 = (layer, options = {}) => {
2103
2119
  alarm(alarmInfo) {
2104
2120
  const logicalAlarms = options.alarms?.pipe(Effect.asVoid);
2105
2121
  const rawAlarm = options.alarm?.(alarmInfo);
2106
- if (logicalAlarms !== void 0 && rawAlarm !== void 0) return this[RunSymbol](Effect.gen(function* () {
2122
+ const alarmEffect = logicalAlarms !== void 0 && rawAlarm !== void 0 ? Effect.gen(function* () {
2107
2123
  yield* logicalAlarms;
2108
2124
  yield* rawAlarm;
2109
- }));
2110
- if (logicalAlarms !== void 0) return this[RunSymbol](logicalAlarms);
2111
- if (rawAlarm !== void 0) return this[RunSymbol](rawAlarm);
2125
+ }) : logicalAlarms ?? rawAlarm;
2126
+ if (alarmEffect !== void 0) return this[RunSymbol](alarmEffect.pipe(Effect.onExit(() => scheduleTelemetryFlush)));
2112
2127
  }
2113
2128
  webSocketMessage(socket, message) {
2114
2129
  if (options.webSocketMessage !== void 0) return this[RunSymbol](options.webSocketMessage(fromWebSocket(socket), message));
@@ -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.1",
3
+ "version": "0.25.3",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -3,8 +3,13 @@
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, Worker or
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
13
  *
9
14
  * ```ts
10
15
  * 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>>;
@@ -110,6 +121,12 @@ export interface DurableObjectOptions<
110
121
  * the Durable Object's single managed runtime boundary.
111
122
  */
112
123
  readonly alarms?: Effect.Effect<unknown, unknown, HandlerContext<RRuntime | REvent>>;
124
+ /**
125
+ * Optional raw alarm handler.
126
+ *
127
+ * After every invocation, the configured OTLP flusher is scheduled through
128
+ * `DurableObjectState.waitUntil`, including when the handler fails.
129
+ */
113
130
  readonly alarm?: (
114
131
  alarmInfo?: globalThis.AlarmInvocationInfo,
115
132
  ) => Effect.Effect<void, unknown, HandlerContext<RRuntime | REvent>>;
@@ -193,22 +210,16 @@ export const make = <
193
210
  alarm(alarmInfo?: globalThis.AlarmInvocationInfo): Promise<void> | void {
194
211
  const logicalAlarms = options.alarms?.pipe(Effect.asVoid);
195
212
  const rawAlarm = options.alarm?.(alarmInfo);
196
-
197
- if (logicalAlarms !== undefined && rawAlarm !== undefined) {
198
- return this[RunSymbol](
199
- Effect.gen(function* () {
200
- yield* logicalAlarms;
201
- yield* rawAlarm;
202
- }),
203
- );
204
- }
205
-
206
- if (logicalAlarms !== undefined) {
207
- return this[RunSymbol](logicalAlarms);
208
- }
209
-
210
- if (rawAlarm !== undefined) {
211
- return this[RunSymbol](rawAlarm);
213
+ const alarmEffect =
214
+ logicalAlarms !== undefined && rawAlarm !== undefined
215
+ ? Effect.gen(function* () {
216
+ yield* logicalAlarms;
217
+ yield* rawAlarm;
218
+ })
219
+ : (logicalAlarms ?? rawAlarm);
220
+
221
+ if (alarmEffect !== undefined) {
222
+ return this[RunSymbol](alarmEffect.pipe(Effect.onExit(() => scheduleTelemetryFlush)));
212
223
  }
213
224
  }
214
225
 
@@ -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>>(
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));