effect-cf 0.36.0 → 0.38.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,19 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.38.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#137](https://github.com/danieljvdm/effect-cf/pull/137) [`8e35961`](https://github.com/danieljvdm/effect-cf/commit/8e3596157732b487f8fef5fd983a6c56a32e74ec) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add `CloudflareTracer.layer` to send existing Effect spans to Cloudflare Workers Observability. Provide it per invocation through `Worker.make`'s `eventLayer` and enable tracing in Wrangler to see Effect operations alongside automatic platform spans, with context preserved across concurrent fibers.
8
+
9
+ ## 0.37.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#135](https://github.com/danieljvdm/effect-cf/pull/135) [`9dea62e`](https://github.com/danieljvdm/effect-cf/commit/9dea62e74818b7fc9da919e8bdbfcc49991aef36) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add `DurableObjectAlarm.transaction` to commit application storage and logical alarm schedule, replacement, or cancellation together in one native SQLite Durable Object transaction. Application queries can use the existing storage wrapper or a `SqlClient` from `@effect/sql-sqlite-do` backed by the same object. The callback receives transaction-only alarm mutations and preserves the caller's success value, typed errors, and service requirements.
14
+
15
+ Failures before commit roll back application rows, logical alarms, and native alarm reconciliation together. Interruption or a lost reply after commit does not undo committed state. Keep external effects outside the transaction and durably pre-arm a later wake before fallible external work; Cloudflare's native alarm retries remain bounded.
16
+
3
17
  ## 0.36.0
4
18
 
5
19
  ### Minor Changes
package/README.md CHANGED
@@ -47,6 +47,53 @@ Declare `SETTINGS` in `wrangler.jsonc` and pass `SettingsLive` to `Worker.make`.
47
47
 
48
48
  The [counter example](https://github.com/danieljvdm/effect-cf/tree/main/examples/counter) shows `DurableObject.Tag`, storage, a typed RPC call, and the complete Wrangler configuration.
49
49
 
50
+ For atomic application writes and alarm changes, see the [alarm transaction example](tests/fixtures/alarm-transaction-consumer.ts) and [API contract](src/DurableObjectAlarm.ts).
51
+
52
+ ## Cloudflare Observability traces
53
+
54
+ `CloudflareTracer.layer` sends existing `Effect.withSpan` and named `Effect.fn`
55
+ spans to Cloudflare's trace waterfall, alongside automatic platform spans.
56
+
57
+ ```ts
58
+ import { Effect, Layer } from "effect";
59
+ import { CloudflareTracer, Worker } from "effect-cf";
60
+
61
+ export default Worker.make(Layer.empty, {
62
+ eventLayer: CloudflareTracer.layer,
63
+ fetch: Effect.sync(() => new Response("Hello")).pipe(Effect.withSpan("greet")),
64
+ });
65
+ ```
66
+
67
+ Enable tracing in `wrangler.jsonc`:
68
+
69
+ ```jsonc
70
+ {
71
+ "compatibility_date": "2026-08-25",
72
+ "observability": {
73
+ "traces": { "enabled": true },
74
+ },
75
+ }
76
+ ```
77
+
78
+ Build the layer per invocation with `eventLayer`. It captures the current
79
+ Cloudflare async context, so do not put it in a runtime layer cached across
80
+ requests. Standalone Effect programs can provide it around each invocation.
81
+ Nested spans, concurrent fibers, and resumed work restore the appropriate span
82
+ context. Cloudflare handles sampling and export; no exporter endpoint or flush
83
+ is needed.
84
+
85
+ String, number, and boolean attributes are forwarded. Other attributes, span
86
+ events, and links remain Effect-local. Completion is recorded as `effect.exit`
87
+ with `success`, `failure`, or `interrupted`; Cloudflare does not expose a setter
88
+ for native outcome status. Effect IDs remain separate from Cloudflare's opaque
89
+ trace/span IDs. Explicit external parents cannot join a Cloudflare trace by ID,
90
+ and `root: true` starts under the invocation's captured context.
91
+
92
+ This layer replaces the active Effect tracer. When combining it with
93
+ `CloudflareOtlp`, select only `logs` and/or `metrics` in the OTLP layer. See
94
+ [Cloudflare's custom span API](https://developers.cloudflare.com/workers/observability/traces/custom-spans/)
95
+ for platform limitations.
96
+
50
97
  ## Native RPC tracing
51
98
 
52
99
  `call()`, `scopedCall()`, and definition methods create one CLIENT span named `binding/method`, covering argument encoding, the native RPC wait, and success decoding. Raw `rpc()` retains Cloudflare's pipelined result without creating a span. Wrap its complete lifetime with `RpcTracing.withRpcClientSpan` when tracing raw calls.
@@ -1,4 +1,4 @@
1
- import { t as DurableObjectState } from "./DurableObjectState-DCh4PZMN.mjs";
1
+ import { t as DurableObjectState } from "./DurableObjectState-DLL0YgBd.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";
@@ -288,4 +288,4 @@ const runExitPreservingTypedFailures = async (run, effect) => {
288
288
  return exit;
289
289
  };
290
290
  //#endregion
291
- export { DurableWebSocketAttachmentError as a, DurableObjectWebSocket_exports as i, DurableObjectState_exports as n, fromWebSocket as o, fromDurableObjectState as r, DurableObjectStorage_exports as s, DurableObjectState as t };
291
+ export { DurableWebSocketAttachmentError as a, StorageOperationError as c, DurableObjectWebSocket_exports as i, DurableObjectState_exports as n, fromWebSocket as o, fromDurableObjectState as r, DurableObjectStorage_exports as s, DurableObjectState as t };
package/dist/Vitest.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as WorkerEnvironment, i as WorkerConfig } from "./ErrorMessage-Byx2OgNd.mjs";
2
2
  import { r as runNativeCallback } from "./WorkerContext-Yobnfw-K.mjs";
3
- import { r as fromDurableObjectState, t as DurableObjectState } from "./DurableObjectState-DCh4PZMN.mjs";
3
+ import { r as fromDurableObjectState, t as DurableObjectState } from "./DurableObjectState-DLL0YgBd.mjs";
4
4
  import { ConfigProvider, Effect, Exit, Layer } from "effect";
5
5
  import { env } from "cloudflare:workers";
6
6
  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";
package/dist/index.d.mts CHANGED
@@ -8,8 +8,8 @@ import { a as WorkflowInstanceCreateOptions, i as WorkflowInstanceCreateBatchOpt
8
8
  import { Cause, Config, Context, Data, DateTime, Duration, Effect, Layer, Option, Predicate, Redacted, Schema, Scope } from "effect";
9
9
  import { Headers as Headers$1, HttpClient, HttpClientResponse } from "effect/unstable/http";
10
10
  import { OtlpExporter, OtlpResource } from "effect/unstable/observability";
11
- import { D1Client } from "@effect/sql-d1";
12
11
  import { DurableObject, WorkflowEntrypoint, WorkflowEvent, WorkflowSleepDuration, WorkflowStep, WorkflowStepConfig, WorkflowStepContext, WorkflowStepEvent, WorkflowTimeoutDuration } from "cloudflare:workers";
12
+ import { D1Client } from "@effect/sql-d1";
13
13
  import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
14
14
  import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
15
15
  import * as RpcServer from "effect/unstable/rpc/RpcServer";
@@ -17,7 +17,7 @@ import { SqliteClient } from "@effect/sql-sqlite-do";
17
17
  import { SqlClient } from "effect/unstable/sql";
18
18
  import * as Tracer$1 from "effect/Tracer";
19
19
  declare namespace WorkersAi_d_exports {
20
- export { LayerOptions$13 as LayerOptions, Tag$12 as Tag, TagClass$11 as TagClass, WorkersAiAsyncBatchResponse, WorkersAiBinding, WorkersAiClient, WorkersAiDefinition, WorkersAiEmbeddingInput, WorkersAiEmbeddingResponse, WorkersAiModelsSearchObject, WorkersAiModelsSearchParams, WorkersAiOperationError, WorkersAiOptions, WorkersAiService, embeddingResponse, isWorkersAiBinding, layer$13 as layer, make$12 as make, makeClient$10 as makeClient };
20
+ export { LayerOptions$13 as LayerOptions, Tag$12 as Tag, TagClass$11 as TagClass, WorkersAiAsyncBatchResponse, WorkersAiBinding, WorkersAiClient, WorkersAiDefinition, WorkersAiEmbeddingInput, WorkersAiEmbeddingResponse, WorkersAiModelsSearchObject, WorkersAiModelsSearchParams, WorkersAiOperationError, WorkersAiOptions, WorkersAiService, embeddingResponse, isWorkersAiBinding, layer$14 as layer, make$12 as make, makeClient$10 as makeClient };
21
21
  }
22
22
  declare const WorkersAiOperationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
23
23
  readonly _tag: "WorkersAiOperationError";
@@ -89,11 +89,11 @@ declare const embeddingResponse: (value: {
89
89
  readonly [embeddingDimensionsKey]?: ReadonlyArray<number>;
90
90
  }) => WorkersAiEmbeddingResponse;
91
91
  declare const makeClient$10: <ModelList extends AiModelListType = AiModels>(definition: WorkersAiDefinition) => (ai: WorkersAiBinding<ModelList>) => WorkersAiClient<ModelList>;
92
- declare const layer$13: <Self, ModelList extends AiModelListType = AiModels>(tag: Context.Service<Self, WorkersAiClient<ModelList>>, definition: WorkersAiDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
92
+ declare const layer$14: <Self, ModelList extends AiModelListType = AiModels>(tag: Context.Service<Self, WorkersAiClient<ModelList>>, definition: WorkersAiDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
93
93
  declare const make$12: <Id extends string>(id: Id) => TagClass$11<WorkersAiService<Id>, Id, AiModels>;
94
94
  declare const Tag$12: <Self>() => <Id extends string, ModelList extends AiModelListType = AiModels>(id: Id) => TagClass$11<Self, Id, ModelList>;
95
95
  declare namespace AiGateway_d_exports {
96
- export { AiGatewayBinding, AiGatewayClient, AiGatewayDefinition, AiGatewayFetchOptions, AiGatewayHeaders, AiGatewayLog, AiGatewayOperationError, AiGatewayPatchLog, AiGatewayProvider, AiGatewayRunOptions, AiGatewayService, AiGatewayUniversalRequest, LayerOptions$12 as LayerOptions, Tag$11 as Tag, TagClass$10 as TagClass, isAiGatewayBinding, layer$12 as layer, make$11 as make, makeClient$9 as makeClient, makeClientFromAi, makeHttpClient };
96
+ export { AiGatewayBinding, AiGatewayClient, AiGatewayDefinition, AiGatewayFetchOptions, AiGatewayHeaders, AiGatewayLog, AiGatewayOperationError, AiGatewayPatchLog, AiGatewayProvider, AiGatewayRunOptions, AiGatewayService, AiGatewayUniversalRequest, LayerOptions$12 as LayerOptions, Tag$11 as Tag, TagClass$10 as TagClass, isAiGatewayBinding, layer$13 as layer, make$11 as make, makeClient$9 as makeClient, makeClientFromAi, makeHttpClient };
97
97
  }
98
98
  declare const AiGatewayOperationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
99
99
  readonly _tag: "AiGatewayOperationError";
@@ -167,11 +167,11 @@ declare const makeHttpClient: (definition: Omit<AiGatewayDefinition, "binding">
167
167
  readonly accountId: string;
168
168
  readonly binding?: string;
169
169
  }, request?: typeof fetch) => AiGatewayClient;
170
- declare const layer$12: <Self>(tag: Context.Service<Self, AiGatewayClient>, definition: AiGatewayDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
170
+ declare const layer$13: <Self>(tag: Context.Service<Self, AiGatewayClient>, definition: AiGatewayDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
171
171
  declare const make$11: <Id extends string>(id: Id) => TagClass$10<AiGatewayService<Id>, Id>;
172
172
  declare const Tag$11: <Self>() => <Id extends string>(id: Id) => TagClass$10<Self, Id>;
173
173
  declare namespace AnalyticsEngine_d_exports {
174
- export { AnalyticsEngineBinding, AnalyticsEngineClient, AnalyticsEngineDataPoint, AnalyticsEngineDataPointSchema, AnalyticsEngineDefinition, AnalyticsEngineFieldValue, AnalyticsEngineFieldValueSchema, AnalyticsEngineInvalidWritePolicy, AnalyticsEngineOperationError, AnalyticsEngineQueryClient, AnalyticsEngineQueryColumn, AnalyticsEngineQueryDefinition, AnalyticsEngineQueryError, AnalyticsEngineQueryOptions, AnalyticsEngineQueryResult, AnalyticsEngineQueryRow, AnalyticsEngineQueryService, AnalyticsEngineService, AnalyticsEngineWriteError, AnalyticsEngineWriteOptions, AnalyticsEngineWritePolicy, AnalyticsEngineWriteValidationError, AnalyticsEngineWriteViolation, LayerOptions$11 as LayerOptions, QueryConfigOptions, QueryTag, QueryTagClass, Tag$10 as Tag, TagClass$9 as TagClass, isAnalyticsEngineDataset, layer$11 as layer, layerFetch, layerFetchConfig, make$10 as make, makeClient$8 as makeClient, makeQuery, makeQueryClient, queryConfig, queryLayer, queryLayerConfig, writeLimits };
174
+ export { AnalyticsEngineBinding, AnalyticsEngineClient, AnalyticsEngineDataPoint, AnalyticsEngineDataPointSchema, AnalyticsEngineDefinition, AnalyticsEngineFieldValue, AnalyticsEngineFieldValueSchema, AnalyticsEngineInvalidWritePolicy, AnalyticsEngineOperationError, AnalyticsEngineQueryClient, AnalyticsEngineQueryColumn, AnalyticsEngineQueryDefinition, AnalyticsEngineQueryError, AnalyticsEngineQueryOptions, AnalyticsEngineQueryResult, AnalyticsEngineQueryRow, AnalyticsEngineQueryService, AnalyticsEngineService, AnalyticsEngineWriteError, AnalyticsEngineWriteOptions, AnalyticsEngineWritePolicy, AnalyticsEngineWriteValidationError, AnalyticsEngineWriteViolation, LayerOptions$11 as LayerOptions, QueryConfigOptions, QueryTag, QueryTagClass, Tag$10 as Tag, TagClass$9 as TagClass, isAnalyticsEngineDataset, layer$12 as layer, layerFetch, layerFetchConfig, make$10 as make, makeClient$8 as makeClient, makeQuery, makeQueryClient, queryConfig, queryLayer, queryLayerConfig, writeLimits };
175
175
  }
176
176
  declare const writeLimits: {
177
177
  readonly maxBlobs: 20;
@@ -322,13 +322,13 @@ declare const queryLayer: <Self>(tag: Context.Service<Self, AnalyticsEngineQuery
322
322
  declare const layerFetch: <Self>(tag: Context.Service<Self, AnalyticsEngineQueryClient>, definition: AnalyticsEngineQueryDefinition) => Layer.Layer<Self, never, never>;
323
323
  declare const queryLayerConfig: <Self>(tag: Context.Service<Self, AnalyticsEngineQueryClient>, config?: Config.Config<AnalyticsEngineQueryDefinition>) => Layer.Layer<Self, Config.ConfigError, HttpClient.HttpClient>;
324
324
  declare const layerFetchConfig: <Self>(tag: Context.Service<Self, AnalyticsEngineQueryClient>, config?: Config.Config<AnalyticsEngineQueryDefinition>) => Layer.Layer<Self, Config.ConfigError, never>;
325
- declare const layer$11: <Self>(tag: Context.Service<Self, AnalyticsEngineClient>, definition: LayerOptions$11) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
325
+ declare const layer$12: <Self>(tag: Context.Service<Self, AnalyticsEngineClient>, definition: LayerOptions$11) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
326
326
  declare const make$10: <Id extends string>(id: Id) => TagClass$9<AnalyticsEngineService<Id>, Id>;
327
327
  declare const makeQuery: <Id extends string>(id: Id) => QueryTagClass<AnalyticsEngineQueryService<Id>, Id>;
328
328
  declare const Tag$10: <Self>() => <Id extends string>(id: Id) => TagClass$9<Self, Id>;
329
329
  declare const QueryTag: <Self>() => <Id extends string>(id: Id) => QueryTagClass<Self, Id>;
330
330
  declare namespace BrowserRendering_d_exports {
331
- export { BrowserRenderingBinding, BrowserRenderingBrowserClient, BrowserRenderingBrowserLike, BrowserRenderingClient, BrowserRenderingConnect, BrowserRenderingDefinition, BrowserRenderingExternalResult, BrowserRenderingLaunch, BrowserRenderingMethodOptions, BrowserRenderingOperationError, BrowserRenderingPageClient, BrowserRenderingPageFunction, BrowserRenderingPageLike, BrowserRenderingService, LayerOptions$10 as LayerOptions, Tag$9 as Tag, TagClass$8 as TagClass, isBrowserRenderingBinding, layer$10 as layer, make$9 as make, makeClient$7 as makeClient };
331
+ export { BrowserRenderingBinding, BrowserRenderingBrowserClient, BrowserRenderingBrowserLike, BrowserRenderingClient, BrowserRenderingConnect, BrowserRenderingDefinition, BrowserRenderingExternalResult, BrowserRenderingLaunch, BrowserRenderingMethodOptions, BrowserRenderingOperationError, BrowserRenderingPageClient, BrowserRenderingPageFunction, BrowserRenderingPageLike, BrowserRenderingService, LayerOptions$10 as LayerOptions, Tag$9 as Tag, TagClass$8 as TagClass, isBrowserRenderingBinding, layer$11 as layer, make$9 as make, makeClient$7 as makeClient };
332
332
  }
333
333
  declare const BrowserRenderingOperationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
334
334
  readonly _tag: "BrowserRenderingOperationError";
@@ -402,11 +402,11 @@ interface TagClass$8<Self, Id extends string> extends Context.ServiceClass<Self,
402
402
  }
403
403
  declare const isBrowserRenderingBinding: <Candidate>(value: Candidate) => value is Candidate & BrowserRenderingBinding;
404
404
  declare const makeClient$7: <RawBinding extends BrowserRenderingBinding = BrowserRenderingBinding>(definition: BrowserRenderingDefinition) => (binding: RawBinding) => BrowserRenderingClient<RawBinding>;
405
- declare const layer$10: <Self>(tag: Context.Service<Self, BrowserRenderingClient>, definition: BrowserRenderingDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
405
+ declare const layer$11: <Self>(tag: Context.Service<Self, BrowserRenderingClient>, definition: BrowserRenderingDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
406
406
  declare const make$9: <Id extends string>(id: Id) => TagClass$8<BrowserRenderingService<Id>, Id>;
407
407
  declare const Tag$9: <Self>() => <Id extends string>(id: Id) => TagClass$8<Self, Id>;
408
408
  declare namespace Cache_d_exports {
409
- export { CacheClient, CacheOperation, CacheOperationError, CacheQueryOptions, CacheRequest, CacheStorage, CacheStorageClient, layer$9 as layer, layerFrom, makeCacheClient, makeClient$6 as makeClient };
409
+ export { CacheClient, CacheOperation, CacheOperationError, CacheQueryOptions, CacheRequest, CacheStorage, CacheStorageClient, layer$10 as layer, layerFrom, makeCacheClient, makeClient$6 as makeClient };
410
410
  }
411
411
  type CacheRequest = CloudflareRequestInfo | URL;
412
412
  type CacheQueryOptions = CloudflareCacheQueryOptions;
@@ -438,9 +438,9 @@ declare class CacheStorage extends CacheStorage_base {}
438
438
  declare const makeCacheClient: (cache: CloudflareCache, name?: string) => CacheClient;
439
439
  declare const makeClient$6: (storage: CloudflareCacheStorage) => CacheStorageClient;
440
440
  declare const layerFrom: (storage: CloudflareCacheStorage) => Layer.Layer<CacheStorage>;
441
- declare const layer$9: Layer.Layer<CacheStorage>;
441
+ declare const layer$10: Layer.Layer<CacheStorage>;
442
442
  declare namespace CloudflareOtlp_d_exports {
443
- export { DurableObjectLayerOptions, LayerOptions$9 as LayerOptions, ResourceOptions, Serialization, Signal, WorkerLayerOptions, layer$8 as layer, layerDurableObject, layerJson, layerProtobuf, layerWorker };
443
+ export { DurableObjectLayerOptions, LayerOptions$9 as LayerOptions, ResourceOptions, Serialization, Signal, WorkerLayerOptions, layer$9 as layer, layerDurableObject, layerJson, layerProtobuf, layerWorker };
444
444
  }
445
445
  type ResourceAttributes = NonNullable<Parameters<typeof OtlpResource.make>[0]["attributes"]>;
446
446
  /** Telemetry signal groups supported by the Cloudflare OTLP layers. */
@@ -504,7 +504,7 @@ interface DurableObjectLayerOptions extends LayerOptions$9 {
504
504
  * The layer provides {@link OtlpExporter.Flusher} for draining buffered
505
505
  * telemetry before the isolate freezes.
506
506
  */
507
- declare const layer$8: (options?: LayerOptions$9) => Layer.Layer<OtlpExporter.Flusher>;
507
+ declare const layer$9: (options?: LayerOptions$9) => Layer.Layer<OtlpExporter.Flusher>;
508
508
  /** Base OTLP telemetry layer forced to JSON serialization. */
509
509
  declare const layerJson: (options?: Omit<LayerOptions$9, "serialization">) => Layer.Layer<OtlpExporter.Flusher>;
510
510
  /** Base OTLP telemetry layer forced to protobuf serialization. */
@@ -526,6 +526,23 @@ declare const layerWorker: (options?: WorkerLayerOptions) => Layer.Layer<OtlpExp
526
526
  * backend can tolerate high-cardinality resource attributes.
527
527
  */
528
528
  declare const layerDurableObject: (options?: DurableObjectLayerOptions) => Layer.Layer<OtlpExporter.Flusher, never, DurableObjectState>;
529
+ declare namespace CloudflareTracer_d_exports {
530
+ export { layer$8 as layer };
531
+ }
532
+ /**
533
+ * Sends Effect spans to Cloudflare Workers Observability using the runtime's
534
+ * custom span API. Build this layer inside each invocation, for example with
535
+ * `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
536
+ * It captures the invocation's async context and must not be cached across requests.
537
+ *
538
+ * Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
539
+ * and export; no OTLP exporter or flush is required. Scalar attributes are
540
+ * forwarded; other attributes, events, and links remain Effect-local. Completion
541
+ * is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
542
+ * Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
543
+ * parents and `root: true` cannot override the invocation's Cloudflare trace.
544
+ */
545
+ declare const layer$8: Layer.Layer<never>;
529
546
  declare namespace D1_d_exports {
530
547
  export { D1Definition, D1Service, D1SqlLayerOptions, Service, TypeId$1 as TypeId, make$8 as make };
531
548
  }
@@ -853,7 +870,7 @@ type DurableObjectClass<Rpc extends DurableObjectRpc<ROut>, ROut> = new (state:
853
870
  */
854
871
  declare const make$6: <ROut, LayerError, REvent = never, EventLayerError = never, const Rpc extends DurableObjectRpc<ROut | REvent> = Record<never, never>>(layer: Layer.Layer<ROut, LayerError, DurableObjectState | WorkerEnvironment>, options?: DurableObjectOptions<ROut, REvent, EventLayerError, Rpc>) => DurableObjectClass<Rpc, ROut | REvent>;
855
872
  declare namespace DurableObjectAlarm_d_exports {
856
- export { AlarmDefinitionConfig, AlarmDefinitionEntry, AlarmDefinitionPayload, AlarmDefinitions, AlarmFailurePolicy, AlarmPayload, AlarmPayloadSchema, AlarmRef, AlarmRetryPolicy, AlarmScheduler, DefinedAlarmEvent, DefinedAlarmHandlers, DurableObjectAlarm, DurableObjectAlarmError, DurableObjectAlarmEvent, InvalidAlarmPayloadError, InvalidAlarmRefError, InvalidProcessDueAlarmsOptionsError, InvalidRepeatEveryError, ProcessDueAlarmsFailure, ProcessDueAlarmsFailureAction, ProcessDueAlarmsHandler, ProcessDueAlarmsMode, ProcessDueAlarmsOptions, ProcessDueAlarmsResult, ScheduleAlarmInput, StoredAlarmDecodeError, define, processDue };
873
+ export { AlarmDefinitionConfig, AlarmDefinitionEntry, AlarmDefinitionPayload, AlarmDefinitions, AlarmFailurePolicy, AlarmPayload, AlarmPayloadSchema, AlarmRef, AlarmRetryPolicy, AlarmScheduler, AlarmTransaction, DefinedAlarmEvent, DefinedAlarmHandlers, DurableObjectAlarm, DurableObjectAlarmError, DurableObjectAlarmEvent, InvalidAlarmPayloadError, InvalidAlarmRefError, InvalidProcessDueAlarmsOptionsError, InvalidRepeatEveryError, ProcessDueAlarmsFailure, ProcessDueAlarmsFailureAction, ProcessDueAlarmsHandler, ProcessDueAlarmsMode, ProcessDueAlarmsOptions, ProcessDueAlarmsResult, ScheduleAlarmInput, StoredAlarmDecodeError, define, processDue };
857
874
  }
858
875
  type AlarmPayload = Schema.Json;
859
876
  declare const InvalidAlarmRefError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
@@ -946,8 +963,26 @@ interface ProcessDueAlarmsOptions<OnFailureR = never, OnFailureE = never> {
946
963
  readonly retryFailedAfter?: Duration.Input;
947
964
  }
948
965
  type ProcessDueAlarmsHandler<R = never, E = never> = (event: DurableObjectAlarmEvent) => Effect.Effect<void, E, R>;
966
+ /**
967
+ * Alarm mutations owned by one transaction callback. Run them in the callback's
968
+ * fiber; forked work and use after the callback ends fail with StorageOperationError.
969
+ */
970
+ type AlarmTransaction = Pick<AlarmScheduler, "scheduleAlarm" | "cancelAlarm">;
949
971
  /** Own `storage.setAlarm()` exclusively: a Durable Object has one platform alarm timestamp. */
950
972
  type AlarmScheduler = {
973
+ /**
974
+ * Commits local application storage and logical alarms in one native SQLite
975
+ * Durable Object transaction, reconciling the native alarm before commit.
976
+ * Use the supplied mutations, not standalone alarm methods or nested transactions.
977
+ * SqlClient queries must use this same Durable Object's storage.
978
+ *
979
+ * Failure, defects and interruption before commit roll back. A lost reply or
980
+ * interruption after commit does not undo committed state. Keep RPC and other
981
+ * external effects outside; atomically pre-arm a later wake before fallible work.
982
+ * Cloudflare's native retries are bounded; composition does not remove the
983
+ * pre-arm requirement or promise infinite retry liveness.
984
+ */
985
+ readonly transaction: <A, E, R>(closure: (alarms: AlarmTransaction) => Effect.Effect<A, E, R>) => Effect.Effect<A, E | StorageOperationError, R>;
951
986
  readonly cancelAlarm: (input: AlarmRef) => Effect.Effect<void, InvalidAlarmRefError | StorageOperationError>;
952
987
  /** Acknowledge after handling. Conditional writes preserve handler replacements; alarms are at-least-once. */
953
988
  readonly processDueAlarms: <R = never, E = never, OnFailureR = never, OnFailureE = never>(handle: ProcessDueAlarmsHandler<R, E>, options?: ProcessDueAlarmsOptions<OnFailureR, OnFailureE>) => Effect.Effect<ProcessDueAlarmsResult, E | OnFailureE | DurableObjectAlarmError, R | OnFailureR>;
@@ -1763,4 +1798,4 @@ declare const implement: typeof implement$1;
1763
1798
  type Handler<ROut, Self extends Definition$1.Any> = Handler$1<ROut, Self>;
1764
1799
  type Options<ROut, Self extends Definition$1.Any> = Options$1<ROut, Self>;
1765
1800
  //#endregion
1766
- export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTracing_d_exports as RpcTracing, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
1801
+ export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTracing_d_exports as RpcTracing, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
package/dist/index.mjs CHANGED
@@ -1,15 +1,16 @@
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-Byx2OgNd.mjs";
3
- import { n as Service$1, r as layer$17, t as Binding_exports } from "./Binding-C82jRhdr.mjs";
3
+ import { n as Service$1, r as layer$18, t as Binding_exports } from "./Binding-C82jRhdr.mjs";
4
4
  import { n as Artifacts_exports } from "./Artifacts-CBxWGeZB.mjs";
5
5
  import { t as fromExecutionContext } from "./WorkerContext-Yobnfw-K.mjs";
6
- import { a as DurableWebSocketAttachmentError, i as DurableObjectWebSocket_exports, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState } from "./DurableObjectState-DCh4PZMN.mjs";
6
+ import { a as DurableWebSocketAttachmentError, c as StorageOperationError, i as DurableObjectWebSocket_exports, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState } from "./DurableObjectState-DLL0YgBd.mjs";
7
7
  import { A as QueueDefinition_exports, C as decodeSuccess, D as method$1, E as make$15, O as reservedMethodNames$2, S as decodeArgs, T as encodeSuccess, _ as RpcTracing_exports, a as WorkerDefinition_exports, b as RpcDefinition_exports, c as resolve, d as makeEntrypointRuntime, f as runEventPromise, g as recordDecodedArgs, h as invokeRpcMethod, i as Worker_exports, j as QueueBinding_exports, k as Queue_exports, l as scoped, m as defineEntrypointRpcMethods, n as NativeRequest, o as ServiceBinding_exports, p as assumeEntrypointClass, r as WorkerContext, s as Rpc_exports, t as ExecutionContext, u as scheduleTelemetryFlush$1, v as withRpcClientSpan, w as encodeArgs, x as assertNoReservedMethods, y as withRpcTraceContext } from "./Worker-XsGJsCJX.mjs";
8
8
  import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, FiberSet, Function as Function$1, Layer, Option, Predicate, Queue, Result, Schema, Tracer } from "effect";
9
9
  import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
10
10
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
11
+ import { AsyncLocalStorage } from "node:async_hooks";
12
+ import { DurableObject, WorkflowEntrypoint, tracing } from "cloudflare:workers";
11
13
  import { D1Client } from "@effect/sql-d1";
12
- import { DurableObject, WorkflowEntrypoint } from "cloudflare:workers";
13
14
  import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
14
15
  import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
15
16
  import * as RpcServer from "effect/unstable/rpc/RpcServer";
@@ -21,7 +22,7 @@ var WorkersAi_exports = /* @__PURE__ */ __exportAll({
21
22
  WorkersAiOperationError: () => WorkersAiOperationError,
22
23
  embeddingResponse: () => embeddingResponse,
23
24
  isWorkersAiBinding: () => isWorkersAiBinding,
24
- layer: () => layer$16,
25
+ layer: () => layer$17,
25
26
  make: () => make$14,
26
27
  makeClient: () => makeClient$13
27
28
  });
@@ -78,11 +79,11 @@ const makeClient$13 = (definition) => (ai) => {
78
79
  rawUnsafe: Effect.succeed(ai)
79
80
  };
80
81
  };
81
- const layer$16 = (tag, definition) => layer$17(tag, definition.binding, (value) => isWorkersAiBinding(value), makeClient$13(definition), { expected: expectedWorkersAiBinding });
82
+ const layer$17 = (tag, definition) => layer$18(tag, definition.binding, (value) => isWorkersAiBinding(value), makeClient$13(definition), { expected: expectedWorkersAiBinding });
82
83
  const make$14 = (id) => Tag$13()(id);
83
84
  const Tag$13 = () => (id) => {
84
85
  const tag = Context.Service()(id);
85
- const makeLayer = (definition) => layer$16(tag, definition);
86
+ const makeLayer = (definition) => layer$17(tag, definition);
86
87
  return Object.assign(tag, {
87
88
  id,
88
89
  layer: makeLayer
@@ -94,7 +95,7 @@ var AiGateway_exports = /* @__PURE__ */ __exportAll({
94
95
  AiGatewayOperationError: () => AiGatewayOperationError,
95
96
  Tag: () => Tag$12,
96
97
  isAiGatewayBinding: () => isAiGatewayBinding,
97
- layer: () => layer$15,
98
+ layer: () => layer$16,
98
99
  make: () => make$13,
99
100
  makeClient: () => makeClient$12,
100
101
  makeClientFromAi: () => makeClientFromAi,
@@ -247,11 +248,11 @@ const makeHttpClient = (definition, request = fetch) => {
247
248
  rawUnsafe: Effect.fail(aiGatewayError(binding, "rawUnsafe", /* @__PURE__ */ new TypeError("HTTP AI Gateway clients have no raw binding")))
248
249
  };
249
250
  };
250
- const layer$15 = (tag, definition) => layer$17(tag, definition.binding, isAiGatewayBinding, (ai) => makeClientFromAi(definition, ai), { expected: expectedAiGatewayBinding });
251
+ const layer$16 = (tag, definition) => layer$18(tag, definition.binding, isAiGatewayBinding, (ai) => makeClientFromAi(definition, ai), { expected: expectedAiGatewayBinding });
251
252
  const make$13 = (id) => Tag$12()(id);
252
253
  const Tag$12 = () => (id) => {
253
254
  const tag = Context.Service()(id);
254
- const makeLayer = (definition) => layer$15(tag, definition);
255
+ const makeLayer = (definition) => layer$16(tag, definition);
255
256
  return Object.assign(tag, {
256
257
  id,
257
258
  layer: makeLayer
@@ -268,7 +269,7 @@ var AnalyticsEngine_exports = /* @__PURE__ */ __exportAll({
268
269
  QueryTag: () => QueryTag,
269
270
  Tag: () => Tag$11,
270
271
  isAnalyticsEngineDataset: () => isAnalyticsEngineDataset,
271
- layer: () => layer$14,
272
+ layer: () => layer$15,
272
273
  layerFetch: () => layerFetch,
273
274
  layerFetchConfig: () => layerFetchConfig,
274
275
  make: () => make$12,
@@ -491,12 +492,12 @@ const queryLayerConfig = (tag, config = queryConfig()) => Layer.effect(tag, Effe
491
492
  return yield* makeQueryClient(definition);
492
493
  }));
493
494
  const layerFetchConfig = (tag, config = queryConfig()) => queryLayerConfig(tag, config).pipe(Layer.provide(FetchHttpClient.layer));
494
- const layer$14 = (tag, definition) => layer$17(tag, definition.binding, isAnalyticsEngineDataset, makeClient$11(definition, definition.write), { expected: expectedAnalyticsEngineDataset });
495
+ const layer$15 = (tag, definition) => layer$18(tag, definition.binding, isAnalyticsEngineDataset, makeClient$11(definition, definition.write), { expected: expectedAnalyticsEngineDataset });
495
496
  const make$12 = (id) => Tag$11()(id);
496
497
  const makeQuery = (id) => QueryTag()(id);
497
498
  const Tag$11 = () => (id) => {
498
499
  const tag = Context.Service()(id);
499
- const makeLayer = (definition) => layer$14(tag, definition);
500
+ const makeLayer = (definition) => layer$15(tag, definition);
500
501
  return Object.assign(tag, {
501
502
  id,
502
503
  layer: makeLayer
@@ -522,7 +523,7 @@ var BrowserRendering_exports = /* @__PURE__ */ __exportAll({
522
523
  BrowserRenderingOperationError: () => BrowserRenderingOperationError,
523
524
  Tag: () => Tag$10,
524
525
  isBrowserRenderingBinding: () => isBrowserRenderingBinding,
525
- layer: () => layer$13,
526
+ layer: () => layer$14,
526
527
  make: () => make$11,
527
528
  makeClient: () => makeClient$10
528
529
  });
@@ -579,11 +580,11 @@ const makeClient$10 = (definition) => (binding) => ({
579
580
  connectWith: (connect, options) => tryBrowserRenderingPromise(definition.binding, "connect", () => connect(binding, options)).pipe(Effect.map((browser) => wrapBrowser(definition.binding, browser)), Effect.withSpan("BrowserRendering.connectWith")),
580
581
  rawUnsafe: Effect.succeed(binding)
581
582
  });
582
- const layer$13 = (tag, definition) => layer$17(tag, definition.binding, isBrowserRenderingBinding, makeClient$10(definition), { expected: expectedBrowserRenderingBinding });
583
+ const layer$14 = (tag, definition) => layer$18(tag, definition.binding, isBrowserRenderingBinding, makeClient$10(definition), { expected: expectedBrowserRenderingBinding });
583
584
  const make$11 = (id) => Tag$10()(id);
584
585
  const Tag$10 = () => (id) => {
585
586
  const tag = Context.Service()(id);
586
- const makeLayer = (definition) => layer$13(tag, definition);
587
+ const makeLayer = (definition) => layer$14(tag, definition);
587
588
  return Object.assign(tag, {
588
589
  id,
589
590
  layer: makeLayer
@@ -594,7 +595,7 @@ const Tag$10 = () => (id) => {
594
595
  var Cache_exports = /* @__PURE__ */ __exportAll({
595
596
  CacheOperationError: () => CacheOperationError,
596
597
  CacheStorage: () => CacheStorage,
597
- layer: () => layer$12,
598
+ layer: () => layer$13,
598
599
  layerFrom: () => layerFrom,
599
600
  makeCacheClient: () => makeCacheClient,
600
601
  makeClient: () => makeClient$9
@@ -631,7 +632,7 @@ const makeClient$9 = (storage) => ({
631
632
  rawUnsafe: Effect.succeed(storage)
632
633
  });
633
634
  const layerFrom = (storage) => Layer.succeed(CacheStorage, makeClient$9(storage));
634
- const layer$12 = Layer.sync(CacheStorage, () => makeClient$9(caches));
635
+ const layer$13 = Layer.sync(CacheStorage, () => makeClient$9(caches));
635
636
  //#endregion
636
637
  //#region src/CloudflareOtlp.ts
637
638
  /**
@@ -641,7 +642,7 @@ const layer$12 = Layer.sync(CacheStorage, () => makeClient$9(caches));
641
642
  * layer finalizers run separately and follow their configured shutdown timeout.
642
643
  */
643
644
  var CloudflareOtlp_exports = /* @__PURE__ */ __exportAll({
644
- layer: () => layer$11,
645
+ layer: () => layer$12,
645
646
  layerDurableObject: () => layerDurableObject,
646
647
  layerJson: () => layerJson,
647
648
  layerProtobuf: () => layerProtobuf,
@@ -707,14 +708,14 @@ const makeLayer = (options = {}, runtimeAttributes = {}) => {
707
708
  * The layer provides {@link OtlpExporter.Flusher} for draining buffered
708
709
  * telemetry before the isolate freezes.
709
710
  */
710
- const layer$11 = (options = {}) => makeLayer(options);
711
+ const layer$12 = (options = {}) => makeLayer(options);
711
712
  /** Base OTLP telemetry layer forced to JSON serialization. */
712
- const layerJson = (options = {}) => layer$11({
713
+ const layerJson = (options = {}) => layer$12({
713
714
  ...options,
714
715
  serialization: "json"
715
716
  });
716
717
  /** Base OTLP telemetry layer forced to protobuf serialization. */
717
- const layerProtobuf = (options = {}) => layer$11({
718
+ const layerProtobuf = (options = {}) => layer$12({
718
719
  ...options,
719
720
  serialization: "protobuf"
720
721
  });
@@ -743,6 +744,64 @@ const layerDurableObject = (options = {}) => Layer.unwrap(Effect.map(DurableObje
743
744
  "cloudflare.durable_object.id": options.includeObjectId ? state.id.toString() : void 0
744
745
  })));
745
746
  //#endregion
747
+ //#region src/CloudflareTracer.ts
748
+ var CloudflareTracer_exports = /* @__PURE__ */ __exportAll({ layer: () => layer$11 });
749
+ var Span = class extends Tracer.NativeSpan {
750
+ runInContext;
751
+ cloudflareSpan;
752
+ constructor(options, runInContext, cloudflareSpan) {
753
+ super({
754
+ ...options,
755
+ sampled: options.sampled && (cloudflareSpan?.isTraced ?? false)
756
+ });
757
+ this.runInContext = runInContext;
758
+ this.cloudflareSpan = cloudflareSpan;
759
+ }
760
+ attribute(key, value) {
761
+ super.attribute(key, value);
762
+ if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) this.cloudflareSpan?.setAttribute(key, value);
763
+ }
764
+ end(endTime, exit) {
765
+ if (this.status._tag === "Ended") return;
766
+ super.end(endTime, exit);
767
+ this.cloudflareSpan?.setAttribute("effect.exit", Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure");
768
+ this.cloudflareSpan?.end();
769
+ }
770
+ };
771
+ /**
772
+ * Sends Effect spans to Cloudflare Workers Observability using the runtime's
773
+ * custom span API. Build this layer inside each invocation, for example with
774
+ * `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
775
+ * It captures the invocation's async context and must not be cached across requests.
776
+ *
777
+ * Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
778
+ * and export; no OTLP exporter or flush is required. Scalar attributes are
779
+ * forwarded; other attributes, events, and links remain Effect-local. Completion
780
+ * is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
781
+ * Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
782
+ * parents and `root: true` cannot override the invocation's Cloudflare trace.
783
+ */
784
+ const layer$11 = Layer.effect(Tracer.Tracer, Effect.sync(() => {
785
+ const invocationContext = AsyncLocalStorage.snapshot();
786
+ const contextFor = (span) => {
787
+ while (span?._tag === "Span") {
788
+ if (span instanceof Span) return span.runInContext;
789
+ span = Option.getOrUndefined(span.parent);
790
+ }
791
+ return invocationContext;
792
+ };
793
+ return Tracer.make({
794
+ span(options) {
795
+ const parentContext = options.root ? invocationContext : contextFor(Option.getOrUndefined(options.parent));
796
+ if (!options.sampled) return new Span(options, parentContext);
797
+ return parentContext(() => tracing.startActiveSpan(options.name, (span) => new Span(options, AsyncLocalStorage.snapshot(), span)));
798
+ },
799
+ context(primitive, fiber) {
800
+ return contextFor(fiber.currentSpan)(() => primitive["~effect/Effect/evaluate"](fiber));
801
+ }
802
+ });
803
+ }));
804
+ //#endregion
746
805
  //#region src/ContainerNamespace.ts
747
806
  var ContainerNamespace_exports = /* @__PURE__ */ __exportAll({
748
807
  ContainerNamespace: () => ContainerNamespace,
@@ -864,7 +923,7 @@ const makeClient$8 = (definition) => (namespace) => {
864
923
  rawUnsafe: Effect.succeed(namespace)
865
924
  };
866
925
  };
867
- const layer$10 = (tag, definition) => layer$17(tag, definition.binding, (value) => isContainerNamespaceResource(value), makeClient$8(definition), { expected: expectedContainerNamespace });
926
+ const layer$10 = (tag, definition) => layer$18(tag, definition.binding, (value) => isContainerNamespaceResource(value), makeClient$8(definition), { expected: expectedContainerNamespace });
868
927
  const make$10 = (id) => Tag$9()(id);
869
928
  const Tag$9 = () => (id) => {
870
929
  const tag = Context.Service()(`effect-cf/Container/${id}`);
@@ -1033,7 +1092,7 @@ const makeClient$7 = (definition) => {
1033
1092
  });
1034
1093
  };
1035
1094
  };
1036
- const layer$9 = (tag, definition) => layer$17(tag, definition.binding, (value) => isDurableObjectNamespaceClient(value), makeClient$7(definition), { expected: expectedDurableObjectNamespace });
1095
+ const layer$9 = (tag, definition) => layer$18(tag, definition.binding, (value) => isDurableObjectNamespaceClient(value), makeClient$7(definition), { expected: expectedDurableObjectNamespace });
1037
1096
  const makeDirectMethods = (rpcDefinition, helpers) => {
1038
1097
  const methods = {};
1039
1098
  if (rpcDefinition !== void 0) {
@@ -1430,25 +1489,35 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
1430
1489
  });
1431
1490
  const cancelAlarm = Effect.fn("DurableObjectAlarm.cancelAlarm")(function* (input) {
1432
1491
  const ref = yield* decodeAlarmRef(input);
1433
- yield* state.storage.transaction(() => Effect.gen(function* () {
1434
- yield* ensureTable(state);
1435
- yield* state.storage.sql.exec(`DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`, getScheduledEventId(ref)).pipe(Effect.asVoid);
1436
- yield* reconcileAlarm();
1437
- }));
1492
+ yield* state.storage.sql.exec(`DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`, getScheduledEventId(ref));
1438
1493
  });
1439
1494
  const scheduleAlarm = Effect.fn("DurableObjectAlarm.scheduleAlarm")(function* (input) {
1440
1495
  const ref = yield* decodeAlarmRef(input);
1441
1496
  const repeatEveryMillis = yield* toRepeatEveryMillis(input.repeatEvery);
1442
1497
  const payload = yield* encodeStoredPayload(input.payload);
1443
1498
  const runAt = DateTime.toEpochMillis(input.runAt);
1444
- yield* state.storage.transaction(() => Effect.gen(function* () {
1445
- yield* ensureTable(state);
1446
- yield* state.storage.sql.exec(`INSERT OR REPLACE INTO effect_cf_scheduled_alarms
1499
+ yield* state.storage.sql.exec(`INSERT OR REPLACE INTO effect_cf_scheduled_alarms
1447
1500
  (storage_id, alarm_id, tag, run_at, repeat_every_ms, payload)
1448
- VALUES (?, ?, ?, ?, ?, ?)`, getScheduledEventId(ref), ref.id, ref.tag, runAt, repeatEveryMillis, payload).pipe(Effect.asVoid);
1449
- yield* reconcileAlarm();
1450
- }));
1501
+ VALUES (?, ?, ?, ?, ?, ?)`, getScheduledEventId(ref), ref.id, ref.tag, runAt, repeatEveryMillis, payload);
1451
1502
  });
1503
+ const transaction = (closure) => state.storage.transaction(() => Effect.withFiber((owner) => {
1504
+ let active = true;
1505
+ const requireActive = (effect) => Effect.withFiber((fiber) => active && fiber === owner ? effect : Effect.fail(new StorageOperationError({
1506
+ operation: "alarm.transaction",
1507
+ cause: /* @__PURE__ */ new Error("Alarm mutations require their active transaction callback")
1508
+ })));
1509
+ return Effect.gen(function* () {
1510
+ yield* ensureTable(state);
1511
+ const result = yield* Effect.suspend(() => closure({
1512
+ cancelAlarm: (input) => requireActive(cancelAlarm(input)),
1513
+ scheduleAlarm: (input) => requireActive(scheduleAlarm(input))
1514
+ })).pipe(Effect.ensuring(Effect.sync(() => {
1515
+ active = false;
1516
+ })));
1517
+ yield* reconcileAlarm();
1518
+ return result;
1519
+ });
1520
+ }));
1452
1521
  const processDueAlarms = Effect.fn("DurableObjectAlarm.processDueAlarms")(function* (handle, options) {
1453
1522
  yield* ensureTable(state);
1454
1523
  const mode = options?.mode ?? "isolated";
@@ -1507,9 +1576,10 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
1507
1576
  };
1508
1577
  });
1509
1578
  return DurableObjectAlarm.of({
1510
- cancelAlarm,
1579
+ cancelAlarm: (input) => transaction((alarms) => alarms.cancelAlarm(input)),
1511
1580
  processDueAlarms,
1512
- scheduleAlarm
1581
+ scheduleAlarm: (input) => transaction((alarms) => alarms.scheduleAlarm(input)),
1582
+ transaction
1513
1583
  });
1514
1584
  }).pipe(Effect.withSpan("DurableObjectAlarm.layer")));
1515
1585
  };
@@ -2282,7 +2352,7 @@ const makeClient$6 = (definition, options) => (email) => {
2282
2352
  rawUnsafe: Effect.succeed(email)
2283
2353
  };
2284
2354
  };
2285
- const layer$6 = (tag, definition) => layer$17(tag, definition.binding, isEmailBinding, makeClient$6(definition, definition.send), { expected: expectedSendEmailBinding });
2355
+ const layer$6 = (tag, definition) => layer$18(tag, definition.binding, isEmailBinding, makeClient$6(definition, definition.send), { expected: expectedSendEmailBinding });
2286
2356
  const make$6 = (id) => Tag$7()(id);
2287
2357
  const Tag$7 = () => (id) => {
2288
2358
  const tag = Context.Service()(id);
@@ -2308,7 +2378,7 @@ const makeClient$5 = (definition) => (hyperdrive) => ({
2308
2378
  connectionString: hyperdrive.connectionString,
2309
2379
  rawUnsafe: Effect.succeed(hyperdrive)
2310
2380
  });
2311
- const layer$5 = (tag, definition) => layer$17(tag, definition.binding, isHyperdrive, makeClient$5(definition), { expected: expectedHyperdrive });
2381
+ const layer$5 = (tag, definition) => layer$18(tag, definition.binding, isHyperdrive, makeClient$5(definition), { expected: expectedHyperdrive });
2312
2382
  const make$5 = (id) => Tag$6()(id);
2313
2383
  const Tag$6 = () => (id) => {
2314
2384
  const tag = Context.Service()(id);
@@ -2410,7 +2480,7 @@ const makeClient$4 = (definition) => (images) => {
2410
2480
  rawUnsafe: Effect.succeed(images)
2411
2481
  };
2412
2482
  };
2413
- const layer$4 = (tag, definition) => layer$17(tag, definition.binding, isImagesBinding, makeClient$4(definition), { expected: expectedImagesBinding });
2483
+ const layer$4 = (tag, definition) => layer$18(tag, definition.binding, isImagesBinding, makeClient$4(definition), { expected: expectedImagesBinding });
2414
2484
  const make$4 = (id) => Tag$5()(id);
2415
2485
  const Tag$5 = () => (id) => {
2416
2486
  const tag = Context.Service()(id);
@@ -2518,7 +2588,7 @@ const makeClient$3 = (definition) => {
2518
2588
  rawUnsafe: Effect.succeed(kv)
2519
2589
  });
2520
2590
  };
2521
- const layer$3 = (tag, definition) => layer$17(tag, definition.binding, isKvNamespace, makeClient$3(definition), { expected: expectedKvNamespace });
2591
+ const layer$3 = (tag, definition) => layer$18(tag, definition.binding, isKvNamespace, makeClient$3(definition), { expected: expectedKvNamespace });
2522
2592
  const makeDefinition$1 = (id, definition) => {
2523
2593
  const kvDefinition = {
2524
2594
  id,
@@ -2639,7 +2709,7 @@ const makeClient$2 = (definition) => {
2639
2709
  };
2640
2710
  };
2641
2711
  };
2642
- const layer$2 = (tag, definition) => layer$17(tag, definition.binding, isR2Bucket, makeClient$2(definition), { expected: expectedR2Bucket });
2712
+ const layer$2 = (tag, definition) => layer$18(tag, definition.binding, isR2Bucket, makeClient$2(definition), { expected: expectedR2Bucket });
2643
2713
  const make$3 = (id) => Tag$3()(id);
2644
2714
  const Tag$3 = () => (id) => {
2645
2715
  const tag = Context.Service()(id);
@@ -2705,7 +2775,7 @@ const makeClient$1 = (definition) => (index) => {
2705
2775
  rawUnsafe: Effect.succeed(index)
2706
2776
  };
2707
2777
  };
2708
- const layer$1 = (tag, definition) => layer$17(tag, definition.binding, isVectorizeBinding, makeClient$1(definition), { expected: expectedVectorizeBinding });
2778
+ const layer$1 = (tag, definition) => layer$18(tag, definition.binding, isVectorizeBinding, makeClient$1(definition), { expected: expectedVectorizeBinding });
2709
2779
  const make$2 = (id) => Tag$2()(id);
2710
2780
  const Tag$2 = () => (id) => {
2711
2781
  const tag = Context.Service()(id);
@@ -2860,7 +2930,7 @@ const makeClient = (definition) => {
2860
2930
  rawUnsafe: Effect.succeed(workflow)
2861
2931
  });
2862
2932
  };
2863
- const layer = (tag, definition) => layer$17(tag, definition.binding, (value) => isWorkflow(value), makeClient(definition), { expected: expectedWorkflow });
2933
+ const layer = (tag, definition) => layer$18(tag, definition.binding, (value) => isWorkflow(value), makeClient(definition), { expected: expectedWorkflow });
2864
2934
  //#endregion
2865
2935
  //#region src/WorkflowDefinition.ts
2866
2936
  var WorkflowDefinition_exports = /* @__PURE__ */ __exportAll({
@@ -2881,7 +2951,7 @@ const make$1 = (id, definition) => Tag$1()(id, definition);
2881
2951
  const Tag$1 = () => (id, definition) => {
2882
2952
  const workflowDefinition = makeDefinition(id, definition);
2883
2953
  const tag = Context.Service()(`effect-cf/Workflow/${id}`);
2884
- const layer$18 = (binding) => layer(tag, {
2954
+ const layer$19 = (binding) => layer(tag, {
2885
2955
  ...binding,
2886
2956
  payload: definition.payload,
2887
2957
  result: definition.result
@@ -2901,7 +2971,7 @@ const Tag$1 = () => (id, definition) => {
2901
2971
  payload: workflowDefinition.payload,
2902
2972
  result: workflowDefinition.result,
2903
2973
  make: workflowDefinition.make,
2904
- layer: layer$18,
2974
+ layer: layer$19,
2905
2975
  create,
2906
2976
  createBatch,
2907
2977
  get,
@@ -3046,4 +3116,4 @@ const waitForEvent = (name, options) => Effect.flatMap(WorkflowStep$1, (workflow
3046
3116
  const Tag = Tag$1;
3047
3117
  const implement = implement$1;
3048
3118
  //#endregion
3049
- export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTracing_exports as RpcTracing, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
3119
+ export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTracing_exports as RpcTracing, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -85,6 +85,7 @@
85
85
  "@platformatic/vfs": "^0.4.0",
86
86
  "effect": "catalog:",
87
87
  "esbuild": "^0.28.1",
88
+ "miniflare": "catalog:",
88
89
  "vite-plus": "catalog:",
89
90
  "vitest": "catalog:"
90
91
  },
@@ -0,0 +1,92 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { tracing } from "cloudflare:workers";
3
+ import { Cause, Effect, Exit, Layer, Option, Predicate, Tracer } from "effect";
4
+
5
+ type SpanOptions = Parameters<Tracer.Tracer["span"]>[0];
6
+ type RunInContext = ReturnType<typeof AsyncLocalStorage.snapshot>;
7
+ type CloudflareSpan = Parameters<Parameters<typeof tracing.startActiveSpan>[1]>[0];
8
+
9
+ class Span extends Tracer.NativeSpan {
10
+ constructor(
11
+ options: SpanOptions,
12
+ readonly runInContext: RunInContext,
13
+ readonly cloudflareSpan?: CloudflareSpan,
14
+ ) {
15
+ super({ ...options, sampled: options.sampled && (cloudflareSpan?.isTraced ?? false) });
16
+ }
17
+
18
+ // Effect's tracer contract accepts arbitrary attributes; only CF-supported scalars cross the boundary.
19
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters
20
+ override attribute(key: string, value: unknown): void {
21
+ super.attribute(key, value);
22
+
23
+ if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) {
24
+ this.cloudflareSpan?.setAttribute(key, value);
25
+ }
26
+ }
27
+
28
+ override end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
29
+ if (this.status._tag === "Ended") return;
30
+
31
+ super.end(endTime, exit);
32
+ this.cloudflareSpan?.setAttribute(
33
+ "effect.exit",
34
+ Exit.isSuccess(exit)
35
+ ? "success"
36
+ : Cause.hasInterruptsOnly(exit.cause)
37
+ ? "interrupted"
38
+ : "failure",
39
+ );
40
+ this.cloudflareSpan?.end();
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Sends Effect spans to Cloudflare Workers Observability using the runtime's
46
+ * custom span API. Build this layer inside each invocation, for example with
47
+ * `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
48
+ * It captures the invocation's async context and must not be cached across requests.
49
+ *
50
+ * Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
51
+ * and export; no OTLP exporter or flush is required. Scalar attributes are
52
+ * forwarded; other attributes, events, and links remain Effect-local. Completion
53
+ * is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
54
+ * Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
55
+ * parents and `root: true` cannot override the invocation's Cloudflare trace.
56
+ */
57
+ export const layer: Layer.Layer<never> = Layer.effect(
58
+ Tracer.Tracer,
59
+ Effect.sync(() => {
60
+ const invocationContext = AsyncLocalStorage.snapshot();
61
+ const contextFor = (span: Tracer.AnySpan | undefined): RunInContext => {
62
+ while (span?._tag === "Span") {
63
+ if (span instanceof Span) return span.runInContext;
64
+
65
+ // Effect can install a no-op span while tracing is locally disabled.
66
+ span = Option.getOrUndefined(span.parent);
67
+ }
68
+
69
+ return invocationContext;
70
+ };
71
+
72
+ return Tracer.make({
73
+ span(options) {
74
+ const parentContext = options.root
75
+ ? invocationContext
76
+ : contextFor(Option.getOrUndefined(options.parent));
77
+
78
+ if (!options.sampled) return new Span(options, parentContext);
79
+
80
+ return parentContext(() =>
81
+ tracing.startActiveSpan(
82
+ options.name,
83
+ (span) => new Span(options, AsyncLocalStorage.snapshot(), span),
84
+ ),
85
+ );
86
+ },
87
+ context(primitive, fiber) {
88
+ return contextFor(fiber.currentSpan)(() => primitive["~effect/Effect/evaluate"](fiber));
89
+ },
90
+ });
91
+ }),
92
+ );
@@ -12,7 +12,7 @@ import {
12
12
  } from "effect";
13
13
 
14
14
  import { DurableObjectState } from "./DurableObjectState";
15
- import type { SqlStorageValue, StorageOperationError } from "./DurableObjectStorage";
15
+ import { type SqlStorageValue, StorageOperationError } from "./DurableObjectStorage";
16
16
  import * as ErrorMessage from "./internal/ErrorMessage";
17
17
 
18
18
  const INIT_TABLE_SQL = `
@@ -177,8 +177,30 @@ export type ProcessDueAlarmsHandler<R = never, E = never> = (
177
177
  event: DurableObjectAlarmEvent,
178
178
  ) => Effect.Effect<void, E, R>;
179
179
 
180
+ /**
181
+ * Alarm mutations owned by one transaction callback. Run them in the callback's
182
+ * fiber; forked work and use after the callback ends fail with StorageOperationError.
183
+ */
184
+ export type AlarmTransaction = Pick<AlarmScheduler, "scheduleAlarm" | "cancelAlarm">;
185
+
180
186
  /** Own `storage.setAlarm()` exclusively: a Durable Object has one platform alarm timestamp. */
181
187
  export type AlarmScheduler = {
188
+ /**
189
+ * Commits local application storage and logical alarms in one native SQLite
190
+ * Durable Object transaction, reconciling the native alarm before commit.
191
+ * Use the supplied mutations, not standalone alarm methods or nested transactions.
192
+ * SqlClient queries must use this same Durable Object's storage.
193
+ *
194
+ * Failure, defects and interruption before commit roll back. A lost reply or
195
+ * interruption after commit does not undo committed state. Keep RPC and other
196
+ * external effects outside; atomically pre-arm a later wake before fallible work.
197
+ * Cloudflare's native retries are bounded; composition does not remove the
198
+ * pre-arm requirement or promise infinite retry liveness.
199
+ */
200
+ readonly transaction: <A, E, R>(
201
+ closure: (alarms: AlarmTransaction) => Effect.Effect<A, E, R>,
202
+ ) => Effect.Effect<A, E | StorageOperationError, R>;
203
+
182
204
  readonly cancelAlarm: (
183
205
  input: AlarmRef,
184
206
  ) => Effect.Effect<void, InvalidAlarmRefError | StorageOperationError>;
@@ -523,17 +545,9 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
523
545
  const cancelAlarm = Effect.fn("DurableObjectAlarm.cancelAlarm")(function* (input: AlarmRef) {
524
546
  const ref = yield* decodeAlarmRef(input);
525
547
 
526
- yield* state.storage.transaction(() =>
527
- Effect.gen(function* () {
528
- yield* ensureTable(state);
529
- yield* state.storage.sql
530
- .exec(
531
- `DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`,
532
- getScheduledEventId(ref),
533
- )
534
- .pipe(Effect.asVoid);
535
- yield* reconcileAlarm();
536
- }),
548
+ yield* state.storage.sql.exec(
549
+ `DELETE FROM effect_cf_scheduled_alarms WHERE storage_id = ?`,
550
+ getScheduledEventId(ref),
537
551
  );
538
552
  });
539
553
 
@@ -545,27 +559,62 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
545
559
  const payload = yield* encodeStoredPayload(input.payload);
546
560
  const runAt = DateTime.toEpochMillis(input.runAt);
547
561
 
548
- yield* state.storage.transaction(() =>
549
- Effect.gen(function* () {
550
- yield* ensureTable(state);
551
- yield* state.storage.sql
552
- .exec(
553
- `INSERT OR REPLACE INTO effect_cf_scheduled_alarms
562
+ yield* state.storage.sql.exec(
563
+ `INSERT OR REPLACE INTO effect_cf_scheduled_alarms
554
564
  (storage_id, alarm_id, tag, run_at, repeat_every_ms, payload)
555
565
  VALUES (?, ?, ?, ?, ?, ?)`,
556
- getScheduledEventId(ref),
557
- ref.id,
558
- ref.tag,
559
- runAt,
560
- repeatEveryMillis,
561
- payload,
562
- )
563
- .pipe(Effect.asVoid);
564
- yield* reconcileAlarm();
565
- }),
566
+ getScheduledEventId(ref),
567
+ ref.id,
568
+ ref.tag,
569
+ runAt,
570
+ repeatEveryMillis,
571
+ payload,
566
572
  );
567
573
  });
568
574
 
575
+ const transaction: AlarmScheduler["transaction"] = (closure) =>
576
+ state.storage.transaction(() =>
577
+ Effect.withFiber((owner) => {
578
+ let active = true;
579
+
580
+ // A callback-owned handle cannot escape via a returned Effect or a
581
+ // detached fiber and write after reconciliation or rollback.
582
+ const requireActive = <A, E>(effect: Effect.Effect<A, E>) =>
583
+ Effect.withFiber<A, E | StorageOperationError>((fiber) =>
584
+ active && fiber === owner
585
+ ? effect
586
+ : Effect.fail(
587
+ new StorageOperationError({
588
+ operation: "alarm.transaction",
589
+ cause: new Error(
590
+ "Alarm mutations require their active transaction callback",
591
+ ),
592
+ }),
593
+ ),
594
+ );
595
+
596
+ return Effect.gen(function* () {
597
+ yield* ensureTable(state);
598
+ const result = yield* Effect.suspend(() =>
599
+ closure({
600
+ cancelAlarm: (input) => requireActive(cancelAlarm(input)),
601
+ scheduleAlarm: (input) => requireActive(scheduleAlarm(input)),
602
+ }),
603
+ ).pipe(
604
+ Effect.ensuring(
605
+ Effect.sync(() => {
606
+ active = false;
607
+ }),
608
+ ),
609
+ );
610
+
611
+ yield* reconcileAlarm();
612
+
613
+ return result;
614
+ });
615
+ }),
616
+ );
617
+
569
618
  const processDueAlarms = Effect.fn("DurableObjectAlarm.processDueAlarms")(function* <
570
619
  R,
571
620
  E,
@@ -669,9 +718,10 @@ export class DurableObjectAlarm extends Context.Service<DurableObjectAlarm, Alar
669
718
  });
670
719
 
671
720
  return DurableObjectAlarm.of({
672
- cancelAlarm,
721
+ cancelAlarm: (input) => transaction((alarms) => alarms.cancelAlarm(input)),
673
722
  processDueAlarms,
674
- scheduleAlarm,
723
+ scheduleAlarm: (input) => transaction((alarms) => alarms.scheduleAlarm(input)),
724
+ transaction,
675
725
  });
676
726
  }).pipe(Effect.withSpan("DurableObjectAlarm.layer")),
677
727
  );
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * as Binding from "./Binding";
5
5
  export * as BrowserRendering from "./BrowserRendering";
6
6
  export * as Cache from "./Cache";
7
7
  export * as CloudflareOtlp from "./CloudflareOtlp";
8
+ export * as CloudflareTracer from "./CloudflareTracer";
8
9
  export * as ContainerNamespace from "./ContainerNamespace";
9
10
  export * as D1 from "./D1";
10
11
  export * as DurableObject from "./DurableObject";