effect-cf 0.42.1 → 0.44.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,53 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.44.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#178](https://github.com/danieljvdm/effect-cf/pull/178) [`9551619`](https://github.com/danieljvdm/effect-cf/commit/95516194672b7245ce3db77d3ee3aa0359901ebc) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Preserve more Effect telemetry in Cloudflare's native spans: bounded JSON strings for structured attributes, searchable Effect and parent IDs, span kind, up to eight link ID pairs, and safe failure/defect/interruption classification. The `effect.*` attribute namespace is now reserved for adapter metadata; move application annotations using that prefix to an application namespace for native export.
8
+
9
+ Add `CloudflareTracer.layerWith({ formatError, spanEvents })` while retaining `CloudflareTracer.layer`. Error details require a caller-supplied sanitizing formatter. Event forwarding is off by default; opting in emits bounded structured logs in the owning span's async context and adds log volume. Unsupported or oversized metadata and formatter/logging failures cannot fail application operations. Correlation does not change Cloudflare trace IDs, parentage, native status, or graph links.
10
+
11
+ ## 0.43.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#174](https://github.com/danieljvdm/effect-cf/pull/174) [`10cd4a9`](https://github.com/danieljvdm/effect-cf/commit/10cd4a9cd162ef38599a53c772cdb19f8e241605) Thanks [@danieljvdm](https://github.com/danieljvdm)! - ## effect-cf
16
+
17
+ Require Effect `^4.0.0-rc.115` and matching `@effect/sql-d1`, `@effect/sql-pg`, and
18
+ `@effect/sql-sqlite-do` peers. Upgrade the Effect family together.
19
+
20
+ Preserve Cloudflare span context with Effect's new fiber representation and close
21
+ request scopes when HEAD or status 204, 205, or 304 omits an Effect streaming body.
22
+ Omitted streams are not started, and request resources and telemetry are finalized.
23
+
24
+ HyperdrivePg now uses Effect's native PostgreSQL driver. Consumers must account for
25
+ its new result codecs (`int8` becomes `bigint`, dates become strings, and timestamps
26
+ become epoch milliseconds), wrap JSON parameters with `sql.json`, and submit one
27
+ statement per query. The driver enables named prepared statements by default;
28
+ pass `prepare: false` for poolers that cannot preserve them.
29
+
30
+ ## effect-webtransport
31
+
32
+ Require Effect `^4.0.0-rc.115` and migrate the Socket adapter to scoped readers and
33
+ writers. Replace `socket.run(handler)` with a scoped acquisition of `socket.reader`
34
+ and a loop over `reader.pull`. Use `writer.write` and `writer.writeAll` after
35
+ acquiring `socket.writer`.
36
+
37
+ Reads now follow consumer demand without dispatching a fiber per chunk. Reader
38
+ scope cleanup still releases stream locks, sends FIN on success, and aborts writes
39
+ on failure. Releasing a writer scope half-closes the write side. Interrupting a
40
+ batch stops unsent frames while leaving the socket available for later writes.
41
+
42
+ Every close, including peer FIN, fails with a `SocketError` containing
43
+ `SocketCloseError`. Remove `closeCodeIsError`, `FromBidirectionalStreamOptions`,
44
+ and `defaultCloseCodeIsError`; handle close reasons in the read loop and use
45
+ `Effect.retry` around the scoped loop to reconnect. WebSocket fallback options now
46
+ accept `highWaterMark`.
47
+
48
+ MessagePack serialization was removed upstream. Use `RpcSerialization.layerNdjson`
49
+ or `RpcSerialization.layerSchemaBinary()` for the byte-stream transport.
50
+
3
51
  ## 0.42.1
4
52
 
5
53
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Cloudflare entrypoints and bindings as Effect services.
4
4
 
5
5
  ```sh
6
- npm install effect-cf "effect@^4.0.0-rc.112"
6
+ npm install effect-cf "effect@^4.0.0-rc.115"
7
7
  ```
8
8
 
9
9
  The repository tests against workerd `1.20260825.1` and `@cloudflare/workers-types@5.20260825.1`. Use `compatibility_date: "2026-08-25"` in Wrangler.
@@ -120,12 +120,82 @@ Nested spans, concurrent fibers, and resumed work restore the appropriate span
120
120
  context. Cloudflare handles sampling and export; no exporter endpoint or flush
121
121
  is needed.
122
122
 
123
- String, number, and boolean attributes are forwarded. Other attributes, span
124
- events, and links remain Effect-local. Completion is recorded as `effect.exit`
125
- with `success`, `failure`, or `interrupted`; Cloudflare does not expose a setter
126
- for native outcome status. Effect IDs remain separate from Cloudflare's opaque
127
- trace/span IDs. Explicit external parents cannot join a Cloudflare trace by ID,
128
- and `root: true` starts under the invocation's captured context.
123
+ Strings, finite numbers, and booleans are forwarded as native scalars. Null,
124
+ plain objects (including null-prototype objects), and dense arrays are encoded
125
+ as JSON strings under the original attribute key. For example,
126
+ `{ routes: ["email", "sms"], retry: false }` becomes the string
127
+ `'{"routes":["email","sms"],"retry":false}'`, not native object/array metadata.
128
+ There is no flattening or generated attribute key per nested field.
129
+
130
+ Each encoded value is limited to **4096 UTF-8 bytes, four container levels, and
131
+ 64 values including the root**. Keys and JSON punctuation count toward bytes.
132
+ The entire value is dropped if it exceeds a limit or contains unsupported input:
133
+ cycles, accessors, undefined, non-finite numbers, bigint, symbols, functions,
134
+ class instances (including errors and Effect `Redacted`), or sparse arrays.
135
+ Only own enumerable string keys and array elements are included; getters and
136
+ `toJSON` are never called. Serialization/host errors are ignored. Dropped updates
137
+ leave any previous native attribute value intact. Original Effect-local values
138
+ remain available. These are adapter limits; Cloudflare can apply further limits.
139
+ Existing scalar strings are forwarded unchanged.
140
+
141
+ The adapter reserves **`effect.*`**; caller attributes in that namespace remain
142
+ Effect-local and cannot overwrite its metadata. Existing annotations using that
143
+ prefix should move to an application namespace if they need native export.
144
+
145
+ | Native attribute | Representation |
146
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
147
+ | `effect.trace_id`, `effect.span_id` | Effect IDs, distinct from Cloudflare's opaque IDs |
148
+ | `effect.parent.trace_id`, `effect.parent.span_id` | Effect parent IDs, when present |
149
+ | `effect.span.kind` | Effect's internal/server/client/producer/consumer kind |
150
+ | `effect.span.links` | JSON array of `{ traceId, spanId }` for the first eight links at span end, including `addLinks` updates |
151
+ | `effect.span.links_dropped` | Omitted link count; the candidate array is dropped whole if it exceeds the JSON limits |
152
+ | `effect.exit` | `success`, `failure`, or `interrupted` |
153
+ | `effect.error.kind` | On failure: `failure`, `defect`, `mixed` (typed failures and defects), or `interrupted` (interruptions only) |
154
+
155
+ Link attributes remain local. Correlation and error metadata add no spans or
156
+ logs. No Cause, error payload, message, stack, schema input, or request body is
157
+ automatically serialized for errors. Optional diagnostics require an explicit
158
+ sanitizing formatter:
159
+
160
+ ```ts
161
+ const tracing = CloudflareTracer.layerWith({
162
+ formatError: () => ({
163
+ type: "OperationFailed",
164
+ message: "The operation could not complete",
165
+ }),
166
+ spanEvents: true,
167
+ });
168
+
169
+ export default Worker.make(Layer.empty, {
170
+ eventLayer: tracing,
171
+ fetch: Effect.sync(() => new Response("Hello")).pipe(Effect.withSpan("greet")),
172
+ });
173
+ ```
174
+
175
+ `formatError` receives the failed span's Cause, including interruption, and may
176
+ return `undefined` or sanitized `type`/`message` strings. These become
177
+ `effect.error.type` and `effect.error.message`; each is dropped above 4096 UTF-8
178
+ bytes. Extra returned fields are ignored. The formatter runs once at end only
179
+ for sampled failed spans; formatter exceptions cannot change the application exit.
180
+ Do not pass through raw errors or use a full Cause formatter here.
181
+
182
+ `spanEvents` defaults to `false`. Opting in forwards at most the first **16 event
183
+ attempts per span** through `console.log` in that span's captured async context.
184
+ Each structured log contains `effect.event` (the original name),
185
+ `effect.event.time_unix_nano` (the original bigint timestamp as a decimal string),
186
+ `effect.trace_id`, `effect.span_id`, and nested `attributes`. The complete log
187
+ uses the same JSON limits, including its envelope; an unsupported/oversized log
188
+ is dropped whole and still consumes one attempt. Unsampled and ended spans emit
189
+ no event logs. Reentrant forwarding and console failures are suppressed. This
190
+ adds log volume and may increase Observability costs; log retention/export also
191
+ depends on your Cloudflare logging configuration.
192
+
193
+ This is searchable correlation and optional **log forwarding**. Cloudflare's
194
+ custom span API does not expose native IDs, manual parent wiring, an outcome
195
+ setter, `addLink`, or `addEvent`. It does not create native graph edges, clickable
196
+ links, native OTel span events, or cross-system propagation. Explicit external
197
+ parents cannot join a Cloudflare trace by ID, and `root: true` starts under the
198
+ invocation's captured context.
129
199
 
130
200
  This layer replaces the active Effect tracer. When combining it with
131
201
  `CloudflareOtlp`, select only `logs` and/or `metrics` in the OTLP layer. See
@@ -235,9 +235,9 @@ const makeClient = (definition, defaults) => (dataset) => {
235
235
  };
236
236
  };
237
237
  const queryConfig = (options) => Config.all({
238
- accountId: options?.accountId ?? Config.string("CLOUDFLARE_ACCOUNT_ID"),
239
- apiToken: options?.apiToken ?? Config.redacted("CLOUDFLARE_API_TOKEN"),
240
- apiBaseUrl: options?.apiBaseUrl ?? Config.string("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl))
238
+ accountId: options?.accountId ?? Config.String("CLOUDFLARE_ACCOUNT_ID"),
239
+ apiToken: options?.apiToken ?? Config.Redacted("CLOUDFLARE_API_TOKEN"),
240
+ apiBaseUrl: options?.apiBaseUrl ?? Config.String("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl))
241
241
  });
242
242
  const queryLayer = (tag, definition) => Layer.effect(tag, makeQueryClient(definition));
243
243
  const layerFetch = (tag, definition) => queryLayer(tag, definition).pipe(Layer.provide(FetchHttpClient.layer));
@@ -1,20 +1,65 @@
1
1
  import * as Layer from "effect/Layer";
2
+ import * as Cause from "effect/Cause";
2
3
  declare namespace CloudflareTracer_d_exports {
3
- export { layer };
4
+ export { ErrorDetails, LayerOptions, layer, layerWith };
4
5
  }
6
+ /** Explicitly sanitized diagnostics. No error payload or Cause is serialized automatically. */
7
+ interface ErrorDetails {
8
+ readonly type?: string;
9
+ readonly message?: string;
10
+ }
11
+ interface LayerOptions {
12
+ /**
13
+ * Called once for a sampled failed span, including interruption. Return only
14
+ * safe diagnostic strings; each is dropped if over 4096 UTF-8 bytes. Throws
15
+ * are ignored. Defaults to classification only, without messages or stacks.
16
+ */
17
+ readonly formatError?: (cause: Cause.Cause<unknown>) => ErrorDetails | undefined;
18
+ /**
19
+ * Forward the first 16 event attempts per sampled span as structured console
20
+ * logs in its captured context. Defaults to false. Each complete log must fit
21
+ * the JSON limits documented on `layer`. This adds billable log volume.
22
+ */
23
+ readonly spanEvents?: boolean;
24
+ }
25
+ /**
26
+ * Configures the per-invocation native tracer. See `layer` for representations
27
+ * and limits. Options do not change Cloudflare sampling or native trace IDs.
28
+ */
29
+ declare const layerWith: (options?: LayerOptions) => Layer.Layer<never>;
5
30
  /**
6
- * Sends Effect spans to Cloudflare Workers Observability using the runtime's
7
- * custom span API. Build this layer inside each invocation, for example with
8
- * `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
9
- * It captures the invocation's async context and must not be cached across requests.
31
+ * Sends Effect spans to Cloudflare Workers Observability. Build inside each
32
+ * invocation, e.g. `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
33
+ * Captures async context; never cache the built layer across requests.
34
+ *
35
+ * Strings, finite numbers and booleans remain native scalars. Null, plain
36
+ * objects and arrays become JSON strings under their original keys, limited to
37
+ * 4096 UTF-8 bytes, 4 container levels and 64 values including the root. Values
38
+ * exceeding any limit are dropped whole, as are cycles, accessors, unsupported
39
+ * values (including undefined, bigint, functions, class instances and Redacted)
40
+ * or serialization failures. No getters or toJSON methods are invoked. Only
41
+ * own enumerable string keys / dense array elements are encoded. No flattening.
42
+ *
43
+ * `effect.*` is reserved for this adapter; caller attributes in that namespace
44
+ * remain local. `effect.trace_id`, `effect.span_id`, `effect.parent.trace_id`,
45
+ * `effect.parent.span_id` and `effect.span.kind` describe Effect identities.
46
+ * At end, `effect.span.links` contains JSON for the first 8 linked Effect ID
47
+ * pairs (including addLinks updates), bounded by the same JSON limits, with
48
+ * omitted count in `effect.span.links_dropped`. Link attributes stay local.
49
+ *
50
+ * `effect.exit` is success/failure/interrupted. Failed spans also have
51
+ * `effect.error.kind`: failure, defect, mixed (failures + defects), or interrupted.
52
+ * No error payloads, messages or stacks are exported unless formatError opts in.
53
+ * Events remain local unless `layerWith({ spanEvents: true })` opts into logs.
54
+ * Local NativeSpan storage retains original attributes, links and events.
10
55
  *
11
- * Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
12
- * and export; no OTLP exporter or flush is required. Scalar attributes are
13
- * forwarded; other attributes, events, and links remain Effect-local. Completion
14
- * is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
15
- * Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
16
- * parents and `root: true` cannot override the invocation's Cloudflare trace.
56
+ * Enable observability.traces.enabled in Wrangler. Cloudflare owns sampling
57
+ * and export; no OTLP exporter or flush is required. Metadata creates no extra
58
+ * spans. Cloudflare IDs remain opaque and distinct: this provides lookup, not
59
+ * native graph links or cross-system propagation. There is no native outcome
60
+ * setter or addEvent API. External parents and root: true cannot override the
61
+ * invocation's Cloudflare trace. See Cloudflare's custom spans documentation.
17
62
  */
18
63
  declare const layer: Layer.Layer<never>;
19
64
  //#endregion
20
- export { CloudflareTracer_d_exports, layer };
65
+ export { CloudflareTracer_d_exports, ErrorDetails, LayerOptions, layer, layerWith };
@@ -9,44 +9,163 @@ import { AsyncLocalStorage } from "node:async_hooks";
9
9
  import { tracing } from "cloudflare:workers";
10
10
  import * as Tracer from "effect/Tracer";
11
11
  //#region src/CloudflareTracer.ts
12
- var CloudflareTracer_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
12
+ var CloudflareTracer_exports = /* @__PURE__ */ __exportAll({
13
+ layer: () => layer,
14
+ layerWith: () => layerWith
15
+ });
16
+ const maxBytes = 4096;
17
+ const maxLinks = 8;
18
+ const maxEvents = 16;
19
+ const textEncoder = new TextEncoder();
20
+ const bestEffort = (f) => {
21
+ try {
22
+ f();
23
+ } catch {}
24
+ };
25
+ const encodeJson = (value) => {
26
+ let bytes = 0;
27
+ let values = 0;
28
+ const parts = [];
29
+ const ancestors = /* @__PURE__ */ new Set();
30
+ const append = (part) => {
31
+ bytes += textEncoder.encode(part).byteLength;
32
+ if (bytes > maxBytes) return false;
33
+ parts.push(part);
34
+ return true;
35
+ };
36
+ const string = (text) => text.length <= maxBytes && append(JSON.stringify(text));
37
+ const visit = (current, depth) => {
38
+ if (++values > 64) return false;
39
+ if (Predicate.isString(current)) return string(current);
40
+ if (current === null || Predicate.isBoolean(current)) return append(String(current));
41
+ if (Predicate.isNumber(current)) return Number.isFinite(current) && append(String(current));
42
+ if (!Predicate.isObjectOrArray(current) || depth === 4 || ancestors.has(current)) return false;
43
+ const array = Array.isArray(current);
44
+ const prototype = Object.getPrototypeOf(current);
45
+ if (!array && prototype !== Object.prototype && prototype !== null) return false;
46
+ if (array && current.length > 64 - values) return false;
47
+ ancestors.add(current);
48
+ if (!append(array ? "[" : "{")) return false;
49
+ let count = 0;
50
+ const member = (key) => {
51
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
52
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) return false;
53
+ if (count++ > 0 && !append(",")) return false;
54
+ if (!array && (!string(key) || !append(":"))) return false;
55
+ return visit(descriptor.value, depth + 1);
56
+ };
57
+ if (array) {
58
+ for (let i = 0; i < current.length; i++) if (!member(String(i))) return false;
59
+ } else for (const key in current) if (Object.hasOwn(current, key) && !member(key)) return false;
60
+ ancestors.delete(current);
61
+ return append(array ? "]" : "}");
62
+ };
63
+ try {
64
+ return visit(value, 0) ? parts.join("") : void 0;
65
+ } catch {
66
+ return;
67
+ }
68
+ };
13
69
  var Span = class extends Tracer.NativeSpan {
14
70
  runInContext;
71
+ config;
72
+ forwardEvent;
15
73
  cloudflareSpan;
16
- constructor(options, runInContext, cloudflareSpan) {
74
+ eventAttempts = 0;
75
+ constructor(options, runInContext, config, forwardEvent, cloudflareSpan) {
17
76
  super({
18
77
  ...options,
19
78
  sampled: options.sampled && (cloudflareSpan?.isTraced ?? false)
20
79
  });
21
80
  this.runInContext = runInContext;
81
+ this.config = config;
82
+ this.forwardEvent = forwardEvent;
22
83
  this.cloudflareSpan = cloudflareSpan;
84
+ if (!this.sampled) return;
85
+ bestEffort(() => {
86
+ this.setAttribute("effect.trace_id", this.traceId);
87
+ this.setAttribute("effect.span_id", this.spanId);
88
+ this.setAttribute("effect.span.kind", this.kind);
89
+ if (Option.isSome(this.parent)) {
90
+ this.setAttribute("effect.parent.trace_id", this.parent.value.traceId);
91
+ this.setAttribute("effect.parent.span_id", this.parent.value.spanId);
92
+ }
93
+ });
94
+ }
95
+ setAttribute(key, value) {
96
+ bestEffort(() => this.cloudflareSpan?.setAttribute(key, value));
23
97
  }
24
98
  attribute(key, value) {
25
99
  super.attribute(key, value);
26
- if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) this.cloudflareSpan?.setAttribute(key, value);
100
+ if (!this.sampled || this.status._tag === "Ended" || key.startsWith("effect.")) return;
101
+ if (Predicate.isString(value) || Predicate.isBoolean(value)) this.setAttribute(key, value);
102
+ else if (Predicate.isNumber(value)) {
103
+ if (Number.isFinite(value)) this.setAttribute(key, value);
104
+ } else {
105
+ const json = encodeJson(value);
106
+ if (json !== void 0) this.setAttribute(key, json);
107
+ }
108
+ }
109
+ event(name, startTime, attributes) {
110
+ super.event(name, startTime, attributes);
111
+ if (!this.config.spanEvents || !this.sampled || this.status._tag === "Ended" || this.eventAttempts >= maxEvents) return;
112
+ this.eventAttempts++;
113
+ bestEffort(() => {
114
+ const json = encodeJson({
115
+ "effect.event": name,
116
+ "effect.event.time_unix_nano": String(startTime),
117
+ "effect.trace_id": this.traceId,
118
+ "effect.span_id": this.spanId,
119
+ attributes: attributes ?? {}
120
+ });
121
+ if (json !== void 0) this.forwardEvent(this.runInContext, json);
122
+ });
27
123
  }
28
124
  end(endTime, exit) {
29
125
  if (this.status._tag === "Ended") return;
30
126
  super.end(endTime, exit);
31
- this.cloudflareSpan?.setAttribute("effect.exit", Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure");
32
- this.cloudflareSpan?.end();
127
+ if (!this.sampled) return;
128
+ this.setAttribute("effect.exit", Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure");
129
+ bestEffort(() => {
130
+ if (this.links.length > 0) {
131
+ const links = this.links.slice(0, maxLinks).map(({ span }) => ({
132
+ traceId: span.traceId,
133
+ spanId: span.spanId
134
+ }));
135
+ const json = encodeJson(links);
136
+ if (json !== void 0) this.setAttribute("effect.span.links", json);
137
+ this.setAttribute("effect.span.links_dropped", this.links.length - (json ? links.length : 0));
138
+ }
139
+ });
140
+ if (Exit.isFailure(exit)) {
141
+ this.setAttribute("effect.error.kind", Cause.hasDies(exit.cause) ? Cause.hasFails(exit.cause) ? "mixed" : "defect" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure");
142
+ bestEffort(() => {
143
+ const details = this.config.formatError?.(exit.cause);
144
+ for (const key of ["type", "message"]) {
145
+ const value = details?.[key];
146
+ if (Predicate.isString(value) && value.length <= maxBytes && textEncoder.encode(value).byteLength <= maxBytes) this.setAttribute(`effect.error.${key}`, value);
147
+ }
148
+ });
149
+ }
150
+ bestEffort(() => this.cloudflareSpan?.end());
33
151
  }
34
152
  };
35
153
  /**
36
- * Sends Effect spans to Cloudflare Workers Observability using the runtime's
37
- * custom span API. Build this layer inside each invocation, for example with
38
- * `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
39
- * It captures the invocation's async context and must not be cached across requests.
40
- *
41
- * Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
42
- * and export; no OTLP exporter or flush is required. Scalar attributes are
43
- * forwarded; other attributes, events, and links remain Effect-local. Completion
44
- * is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
45
- * Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
46
- * parents and `root: true` cannot override the invocation's Cloudflare trace.
154
+ * Configures the per-invocation native tracer. See `layer` for representations
155
+ * and limits. Options do not change Cloudflare sampling or native trace IDs.
47
156
  */
48
- const layer = Layer.effect(Tracer.Tracer, Effect.sync(() => {
157
+ const layerWith = (options = {}) => Layer.effect(Tracer.Tracer, Effect.sync(() => {
49
158
  const invocationContext = AsyncLocalStorage.snapshot();
159
+ let forwarding = false;
160
+ const forwardEvent = (context, json) => {
161
+ if (forwarding) return;
162
+ forwarding = true;
163
+ try {
164
+ context(() => console.log(JSON.parse(json)));
165
+ } finally {
166
+ forwarding = false;
167
+ }
168
+ };
50
169
  const contextFor = (span) => {
51
170
  while (span?._tag === "Span") {
52
171
  if (span instanceof Span) return span.runInContext;
@@ -55,15 +174,49 @@ const layer = Layer.effect(Tracer.Tracer, Effect.sync(() => {
55
174
  return invocationContext;
56
175
  };
57
176
  return Tracer.make({
58
- span(options) {
59
- const parentContext = options.root ? invocationContext : contextFor(Option.getOrUndefined(options.parent));
60
- if (!options.sampled) return new Span(options, parentContext);
61
- return parentContext(() => tracing.startActiveSpan(options.name, (span) => new Span(options, AsyncLocalStorage.snapshot(), span)));
177
+ span(spanOptions) {
178
+ const parentContext = spanOptions.root ? invocationContext : contextFor(Option.getOrUndefined(spanOptions.parent));
179
+ if (!spanOptions.sampled) return new Span(spanOptions, parentContext, options, forwardEvent);
180
+ return parentContext(() => tracing.startActiveSpan(spanOptions.name, (span) => new Span(spanOptions, AsyncLocalStorage.snapshot(), options, forwardEvent, span)));
62
181
  },
63
182
  context(primitive, fiber) {
64
- return contextFor(fiber.currentSpan)(() => primitive["~effect/Effect/evaluate"](fiber));
183
+ return contextFor(fiber.cache.span)(() => primitive["~effect/Effect/evaluate"](fiber));
65
184
  }
66
185
  });
67
186
  }));
187
+ /**
188
+ * Sends Effect spans to Cloudflare Workers Observability. Build inside each
189
+ * invocation, e.g. `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
190
+ * Captures async context; never cache the built layer across requests.
191
+ *
192
+ * Strings, finite numbers and booleans remain native scalars. Null, plain
193
+ * objects and arrays become JSON strings under their original keys, limited to
194
+ * 4096 UTF-8 bytes, 4 container levels and 64 values including the root. Values
195
+ * exceeding any limit are dropped whole, as are cycles, accessors, unsupported
196
+ * values (including undefined, bigint, functions, class instances and Redacted)
197
+ * or serialization failures. No getters or toJSON methods are invoked. Only
198
+ * own enumerable string keys / dense array elements are encoded. No flattening.
199
+ *
200
+ * `effect.*` is reserved for this adapter; caller attributes in that namespace
201
+ * remain local. `effect.trace_id`, `effect.span_id`, `effect.parent.trace_id`,
202
+ * `effect.parent.span_id` and `effect.span.kind` describe Effect identities.
203
+ * At end, `effect.span.links` contains JSON for the first 8 linked Effect ID
204
+ * pairs (including addLinks updates), bounded by the same JSON limits, with
205
+ * omitted count in `effect.span.links_dropped`. Link attributes stay local.
206
+ *
207
+ * `effect.exit` is success/failure/interrupted. Failed spans also have
208
+ * `effect.error.kind`: failure, defect, mixed (failures + defects), or interrupted.
209
+ * No error payloads, messages or stacks are exported unless formatError opts in.
210
+ * Events remain local unless `layerWith({ spanEvents: true })` opts into logs.
211
+ * Local NativeSpan storage retains original attributes, links and events.
212
+ *
213
+ * Enable observability.traces.enabled in Wrangler. Cloudflare owns sampling
214
+ * and export; no OTLP exporter or flush is required. Metadata creates no extra
215
+ * spans. Cloudflare IDs remain opaque and distinct: this provides lookup, not
216
+ * native graph links or cross-system propagation. There is no native outcome
217
+ * setter or addEvent API. External parents and root: true cannot override the
218
+ * invocation's Cloudflare trace. See Cloudflare's custom spans documentation.
219
+ */
220
+ const layer = layerWith();
68
221
  //#endregion
69
- export { CloudflareTracer_exports, layer };
222
+ export { CloudflareTracer_exports, layer, layerWith };
@@ -15,11 +15,11 @@ var Environment_exports = /* @__PURE__ */ __exportAll({
15
15
  var WorkerEnvironment = class extends Context.Service()("effect-cf/WorkerEnvironment") {};
16
16
  let WorkerConfig;
17
17
  (function(_WorkerConfig) {
18
- _WorkerConfig.string = (name) => Config.string(name);
19
- _WorkerConfig.redacted = (name) => Config.redacted(name);
20
- _WorkerConfig.number = (name) => Config.number(name);
21
- _WorkerConfig.integer = (name) => Config.int(name);
22
- _WorkerConfig.boolean = (name) => Config.boolean(name);
18
+ _WorkerConfig.string = (name) => Config.String(name);
19
+ _WorkerConfig.redacted = (name) => Config.Redacted(name);
20
+ _WorkerConfig.number = (name) => Config.Number(name);
21
+ _WorkerConfig.integer = (name) => Config.Int(name);
22
+ _WorkerConfig.boolean = (name) => Config.Boolean(name);
23
23
  const providerFromEnv = _WorkerConfig.providerFromEnv = (env, options) => {
24
24
  const record = {};
25
25
  for (const [key, value] of Object.entries(env)) if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) record[key] = String(value);
package/dist/Worker.mjs CHANGED
@@ -58,8 +58,9 @@ const renderFetchSuccess = (effect, eventLayer) => Effect.suspend(() => {
58
58
  return Effect.succeed(response);
59
59
  });
60
60
  const handleResponse = (request, response) => nativeResponse !== void 0 ? Effect.void : Effect.map(Effect.context(), (context) => {
61
- webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
62
- withoutBody: request.method === "HEAD",
61
+ const withoutBody = request.method === "HEAD";
62
+ webResponse = HttpServerResponse.toWeb(HttpServerResponse.omitsBody(response, withoutBody) ? response : HttpEffect.scopeTransferToStream(response), {
63
+ withoutBody,
63
64
  context
64
65
  });
65
66
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.42.1",
3
+ "version": "0.44.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -94,11 +94,11 @@
94
94
  "@cloudflare/sandbox": "^0.13.0-next.738.2",
95
95
  "@cloudflare/vitest-plugin": "^1.0.0",
96
96
  "@cloudflare/workers-types": "^5.20260825.1",
97
- "@effect/sql-d1": "^4.0.0-rc.112",
98
- "@effect/sql-pg": "^4.0.0-rc.112",
99
- "@effect/sql-sqlite-do": "^4.0.0-rc.112",
97
+ "@effect/sql-d1": "^4.0.0-rc.115",
98
+ "@effect/sql-pg": "^4.0.0-rc.115",
99
+ "@effect/sql-sqlite-do": "^4.0.0-rc.115",
100
100
  "@modelcontextprotocol/server": "^2.0.0",
101
- "effect": "^4.0.0-rc.112"
101
+ "effect": "^4.0.0-rc.115"
102
102
  },
103
103
  "peerDependenciesMeta": {
104
104
  "@cloudflare/computer": {
@@ -729,11 +729,11 @@ export const makeClient =
729
729
 
730
730
  export const queryConfig = (options?: QueryConfigOptions) =>
731
731
  Config.all({
732
- accountId: options?.accountId ?? Config.string("CLOUDFLARE_ACCOUNT_ID"),
733
- apiToken: options?.apiToken ?? Config.redacted("CLOUDFLARE_API_TOKEN"),
732
+ accountId: options?.accountId ?? Config.String("CLOUDFLARE_ACCOUNT_ID"),
733
+ apiToken: options?.apiToken ?? Config.Redacted("CLOUDFLARE_API_TOKEN"),
734
734
  apiBaseUrl:
735
735
  options?.apiBaseUrl ??
736
- Config.string("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl)),
736
+ Config.String("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl)),
737
737
  });
738
738
 
739
739
  export const queryLayer = <Self>(
@@ -11,31 +11,185 @@ import * as Tracer from "effect/Tracer";
11
11
  type SpanOptions = Parameters<Tracer.Tracer["span"]>[0];
12
12
  type RunInContext = ReturnType<typeof AsyncLocalStorage.snapshot>;
13
13
  type CloudflareSpan = Parameters<Parameters<typeof tracing.startActiveSpan>[1]>[0];
14
+ type ForwardEvent = (context: RunInContext, json: string) => void;
15
+
16
+ /** Explicitly sanitized diagnostics. No error payload or Cause is serialized automatically. */
17
+ export interface ErrorDetails {
18
+ readonly type?: string;
19
+ readonly message?: string;
20
+ }
21
+
22
+ export interface LayerOptions {
23
+ /**
24
+ * Called once for a sampled failed span, including interruption. Return only
25
+ * safe diagnostic strings; each is dropped if over 4096 UTF-8 bytes. Throws
26
+ * are ignored. Defaults to classification only, without messages or stacks.
27
+ */
28
+ readonly formatError?: (cause: Cause.Cause<unknown>) => ErrorDetails | undefined;
29
+ /**
30
+ * Forward the first 16 event attempts per sampled span as structured console
31
+ * logs in its captured context. Defaults to false. Each complete log must fit
32
+ * the JSON limits documented on `layer`. This adds billable log volume.
33
+ */
34
+ readonly spanEvents?: boolean;
35
+ }
36
+
37
+ const maxBytes = 4096;
38
+ const maxLinks = 8;
39
+ const maxEvents = 16;
40
+ const textEncoder = new TextEncoder();
41
+
42
+ const bestEffort = (f: () => void): void => {
43
+ try {
44
+ f();
45
+ } catch {
46
+ // Telemetry must not change the application's exit, including formatter/host failures.
47
+ }
48
+ };
49
+
50
+ // This bounded JSON writer deliberately avoids coercion, getters and toJSON.
51
+ // Schema decoding / JSON.stringify on arbitrary input would traverse it before
52
+ // enforcing our work/depth limits, and could invoke application code.
53
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters
54
+ const encodeJson = (value: unknown): string | undefined => {
55
+ let bytes = 0;
56
+ let values = 0;
57
+ const parts: Array<string> = [];
58
+ const ancestors = new Set<object>();
59
+ const append = (part: string): boolean => {
60
+ bytes += textEncoder.encode(part).byteLength;
61
+ if (bytes > maxBytes) return false;
62
+ parts.push(part);
63
+
64
+ return true;
65
+ };
66
+ const string = (text: string): boolean => text.length <= maxBytes && append(JSON.stringify(text));
67
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters
68
+ const visit = (current: unknown, depth: number): boolean => {
69
+ if (++values > 64) return false;
70
+ if (Predicate.isString(current)) return string(current);
71
+ if (current === null || Predicate.isBoolean(current)) return append(String(current));
72
+ if (Predicate.isNumber(current)) return Number.isFinite(current) && append(String(current));
73
+ if (!Predicate.isObjectOrArray(current) || depth === 4 || ancestors.has(current)) return false;
74
+
75
+ const array = Array.isArray(current);
76
+ const prototype = Object.getPrototypeOf(current);
77
+
78
+ if (!array && prototype !== Object.prototype && prototype !== null) return false;
79
+ if (array && current.length > 64 - values) return false;
80
+ ancestors.add(current);
81
+ if (!append(array ? "[" : "{")) return false;
82
+
83
+ let count = 0;
84
+ const member = (key: string): boolean => {
85
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
86
+
87
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) return false;
88
+ if (count++ > 0 && !append(",")) return false;
89
+ if (!array && (!string(key) || !append(":"))) return false;
90
+
91
+ return visit(descriptor.value, depth + 1);
92
+ };
93
+
94
+ if (array) {
95
+ for (let i = 0; i < current.length; i++) {
96
+ if (!member(String(i))) return false;
97
+ }
98
+ } else {
99
+ for (const key in current) {
100
+ if (Object.hasOwn(current, key) && !member(key)) return false;
101
+ }
102
+ }
103
+ ancestors.delete(current);
104
+
105
+ return append(array ? "]" : "}");
106
+ };
107
+
108
+ try {
109
+ return visit(value, 0) ? parts.join("") : undefined;
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ };
14
114
 
15
115
  class Span extends Tracer.NativeSpan {
116
+ private eventAttempts = 0;
117
+
16
118
  constructor(
17
119
  options: SpanOptions,
18
120
  readonly runInContext: RunInContext,
121
+ readonly config: LayerOptions,
122
+ readonly forwardEvent: ForwardEvent,
19
123
  readonly cloudflareSpan?: CloudflareSpan,
20
124
  ) {
21
125
  super({ ...options, sampled: options.sampled && (cloudflareSpan?.isTraced ?? false) });
126
+ if (!this.sampled) return;
127
+
128
+ bestEffort(() => {
129
+ this.setAttribute("effect.trace_id", this.traceId);
130
+ this.setAttribute("effect.span_id", this.spanId);
131
+ this.setAttribute("effect.span.kind", this.kind);
132
+ if (Option.isSome(this.parent)) {
133
+ this.setAttribute("effect.parent.trace_id", this.parent.value.traceId);
134
+ this.setAttribute("effect.parent.span_id", this.parent.value.spanId);
135
+ }
136
+ });
137
+ }
138
+
139
+ private setAttribute(key: string, value: string | number | boolean): void {
140
+ bestEffort(() => this.cloudflareSpan?.setAttribute(key, value));
22
141
  }
23
142
 
24
- // Effect's tracer contract accepts arbitrary attributes; only CF-supported scalars cross the boundary.
143
+ // Effect's tracer contract accepts arbitrary attributes.
25
144
  // oxlint-disable-next-line anti-slop/no-unknown-parameters
26
145
  override attribute(key: string, value: unknown): void {
27
146
  super.attribute(key, value);
147
+ if (!this.sampled || this.status._tag === "Ended" || key.startsWith("effect.")) return;
148
+
149
+ if (Predicate.isString(value) || Predicate.isBoolean(value)) {
150
+ this.setAttribute(key, value);
151
+ } else if (Predicate.isNumber(value)) {
152
+ if (Number.isFinite(value)) this.setAttribute(key, value);
153
+ } else {
154
+ const json = encodeJson(value);
28
155
 
29
- if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) {
30
- this.cloudflareSpan?.setAttribute(key, value);
156
+ if (json !== undefined) this.setAttribute(key, json);
31
157
  }
32
158
  }
33
159
 
160
+ override event(
161
+ name: string,
162
+ startTime: bigint,
163
+ attributes?: Parameters<Tracer.Span["event"]>[2],
164
+ ): void {
165
+ super.event(name, startTime, attributes);
166
+ if (
167
+ !this.config.spanEvents ||
168
+ !this.sampled ||
169
+ this.status._tag === "Ended" ||
170
+ this.eventAttempts >= maxEvents
171
+ )
172
+ return;
173
+ this.eventAttempts++;
174
+ bestEffort(() => {
175
+ const json = encodeJson({
176
+ "effect.event": name,
177
+ "effect.event.time_unix_nano": String(startTime),
178
+ "effect.trace_id": this.traceId,
179
+ "effect.span_id": this.spanId,
180
+ attributes: attributes ?? {},
181
+ });
182
+
183
+ if (json !== undefined) this.forwardEvent(this.runInContext, json);
184
+ });
185
+ }
186
+
34
187
  override end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
35
188
  if (this.status._tag === "Ended") return;
36
-
37
189
  super.end(endTime, exit);
38
- this.cloudflareSpan?.setAttribute(
190
+ if (!this.sampled) return;
191
+
192
+ this.setAttribute(
39
193
  "effect.exit",
40
194
  Exit.isSuccess(exit)
41
195
  ? "success"
@@ -43,56 +197,138 @@ class Span extends Tracer.NativeSpan {
43
197
  ? "interrupted"
44
198
  : "failure",
45
199
  );
46
- this.cloudflareSpan?.end();
200
+ bestEffort(() => {
201
+ if (this.links.length > 0) {
202
+ const links = this.links.slice(0, maxLinks).map(({ span }) => ({
203
+ traceId: span.traceId,
204
+ spanId: span.spanId,
205
+ }));
206
+ const json = encodeJson(links);
207
+
208
+ if (json !== undefined) this.setAttribute("effect.span.links", json);
209
+ this.setAttribute(
210
+ "effect.span.links_dropped",
211
+ this.links.length - (json ? links.length : 0),
212
+ );
213
+ }
214
+ });
215
+ if (Exit.isFailure(exit)) {
216
+ this.setAttribute(
217
+ "effect.error.kind",
218
+ Cause.hasDies(exit.cause)
219
+ ? Cause.hasFails(exit.cause)
220
+ ? "mixed"
221
+ : "defect"
222
+ : Cause.hasInterruptsOnly(exit.cause)
223
+ ? "interrupted"
224
+ : "failure",
225
+ );
226
+ bestEffort(() => {
227
+ const details = this.config.formatError?.(exit.cause);
228
+
229
+ for (const key of ["type", "message"] as const) {
230
+ const value = details?.[key];
231
+
232
+ if (
233
+ Predicate.isString(value) &&
234
+ value.length <= maxBytes &&
235
+ textEncoder.encode(value).byteLength <= maxBytes
236
+ ) {
237
+ this.setAttribute(`effect.error.${key}`, value);
238
+ }
239
+ }
240
+ });
241
+ }
242
+ bestEffort(() => this.cloudflareSpan?.end());
47
243
  }
48
244
  }
49
245
 
50
246
  /**
51
- * Sends Effect spans to Cloudflare Workers Observability using the runtime's
52
- * custom span API. Build this layer inside each invocation, for example with
53
- * `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
54
- * It captures the invocation's async context and must not be cached across requests.
55
- *
56
- * Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
57
- * and export; no OTLP exporter or flush is required. Scalar attributes are
58
- * forwarded; other attributes, events, and links remain Effect-local. Completion
59
- * is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
60
- * Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
61
- * parents and `root: true` cannot override the invocation's Cloudflare trace.
247
+ * Configures the per-invocation native tracer. See `layer` for representations
248
+ * and limits. Options do not change Cloudflare sampling or native trace IDs.
62
249
  */
63
- export const layer: Layer.Layer<never> = Layer.effect(
64
- Tracer.Tracer,
65
- Effect.sync(() => {
66
- const invocationContext = AsyncLocalStorage.snapshot();
67
- const contextFor = (span: Tracer.AnySpan | undefined): RunInContext => {
68
- while (span?._tag === "Span") {
69
- if (span instanceof Span) return span.runInContext;
70
-
71
- // Effect can install a no-op span while tracing is locally disabled.
72
- span = Option.getOrUndefined(span.parent);
73
- }
250
+ export const layerWith = (options: LayerOptions = {}): Layer.Layer<never> =>
251
+ Layer.effect(
252
+ Tracer.Tracer,
253
+ Effect.sync(() => {
254
+ const invocationContext = AsyncLocalStorage.snapshot();
255
+ let forwarding = false;
256
+ const forwardEvent: ForwardEvent = (context, json) => {
257
+ if (forwarding) return;
258
+ forwarding = true;
259
+ try {
260
+ // Parse only our own bounded JSON to give console a structured object.
261
+ context(() => console.log(JSON.parse(json)));
262
+ } finally {
263
+ forwarding = false;
264
+ }
265
+ };
266
+ const contextFor = (span: Tracer.AnySpan | undefined): RunInContext => {
267
+ while (span?._tag === "Span") {
268
+ if (span instanceof Span) return span.runInContext;
74
269
 
75
- return invocationContext;
76
- };
270
+ // Effect can install a no-op span while tracing is locally disabled.
271
+ span = Option.getOrUndefined(span.parent);
272
+ }
77
273
 
78
- return Tracer.make({
79
- span(options) {
80
- const parentContext = options.root
81
- ? invocationContext
82
- : contextFor(Option.getOrUndefined(options.parent));
274
+ return invocationContext;
275
+ };
83
276
 
84
- if (!options.sampled) return new Span(options, parentContext);
277
+ return Tracer.make({
278
+ span(spanOptions) {
279
+ const parentContext = spanOptions.root
280
+ ? invocationContext
281
+ : contextFor(Option.getOrUndefined(spanOptions.parent));
85
282
 
86
- return parentContext(() =>
87
- tracing.startActiveSpan(
88
- options.name,
89
- (span) => new Span(options, AsyncLocalStorage.snapshot(), span),
90
- ),
91
- );
92
- },
93
- context(primitive, fiber) {
94
- return contextFor(fiber.currentSpan)(() => primitive["~effect/Effect/evaluate"](fiber));
95
- },
96
- });
97
- }),
98
- );
283
+ if (!spanOptions.sampled)
284
+ return new Span(spanOptions, parentContext, options, forwardEvent);
285
+
286
+ return parentContext(() =>
287
+ tracing.startActiveSpan(
288
+ spanOptions.name,
289
+ (span) =>
290
+ new Span(spanOptions, AsyncLocalStorage.snapshot(), options, forwardEvent, span),
291
+ ),
292
+ );
293
+ },
294
+ context(primitive, fiber) {
295
+ return contextFor(fiber.cache.span)(() => primitive["~effect/Effect/evaluate"](fiber));
296
+ },
297
+ });
298
+ }),
299
+ );
300
+
301
+ /**
302
+ * Sends Effect spans to Cloudflare Workers Observability. Build inside each
303
+ * invocation, e.g. `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
304
+ * Captures async context; never cache the built layer across requests.
305
+ *
306
+ * Strings, finite numbers and booleans remain native scalars. Null, plain
307
+ * objects and arrays become JSON strings under their original keys, limited to
308
+ * 4096 UTF-8 bytes, 4 container levels and 64 values including the root. Values
309
+ * exceeding any limit are dropped whole, as are cycles, accessors, unsupported
310
+ * values (including undefined, bigint, functions, class instances and Redacted)
311
+ * or serialization failures. No getters or toJSON methods are invoked. Only
312
+ * own enumerable string keys / dense array elements are encoded. No flattening.
313
+ *
314
+ * `effect.*` is reserved for this adapter; caller attributes in that namespace
315
+ * remain local. `effect.trace_id`, `effect.span_id`, `effect.parent.trace_id`,
316
+ * `effect.parent.span_id` and `effect.span.kind` describe Effect identities.
317
+ * At end, `effect.span.links` contains JSON for the first 8 linked Effect ID
318
+ * pairs (including addLinks updates), bounded by the same JSON limits, with
319
+ * omitted count in `effect.span.links_dropped`. Link attributes stay local.
320
+ *
321
+ * `effect.exit` is success/failure/interrupted. Failed spans also have
322
+ * `effect.error.kind`: failure, defect, mixed (failures + defects), or interrupted.
323
+ * No error payloads, messages or stacks are exported unless formatError opts in.
324
+ * Events remain local unless `layerWith({ spanEvents: true })` opts into logs.
325
+ * Local NativeSpan storage retains original attributes, links and events.
326
+ *
327
+ * Enable observability.traces.enabled in Wrangler. Cloudflare owns sampling
328
+ * and export; no OTLP exporter or flush is required. Metadata creates no extra
329
+ * spans. Cloudflare IDs remain opaque and distinct: this provides lookup, not
330
+ * native graph links or cross-system propagation. There is no native outcome
331
+ * setter or addEvent API. External parents and root: true cannot override the
332
+ * invocation's Cloudflare trace. See Cloudflare's custom spans documentation.
333
+ */
334
+ export const layer: Layer.Layer<never> = layerWith();
@@ -33,19 +33,19 @@ export namespace WorkerConfig {
33
33
  export type Key = ScalarConfigKey;
34
34
 
35
35
  /** Read a scalar Cloudflare var or secret as a string. */
36
- export const string = <const Name extends Key>(name: Name) => Config.string(name);
36
+ export const string = <const Name extends Key>(name: Name) => Config.String(name);
37
37
 
38
38
  /** Read a scalar Cloudflare secret as a redacted string. */
39
- export const redacted = <const Name extends Key>(name: Name) => Config.redacted(name);
39
+ export const redacted = <const Name extends Key>(name: Name) => Config.Redacted(name);
40
40
 
41
41
  /** Read a scalar Cloudflare var or secret as a number. */
42
- export const number = <const Name extends Key>(name: Name) => Config.number(name);
42
+ export const number = <const Name extends Key>(name: Name) => Config.Number(name);
43
43
 
44
44
  /** Read a scalar Cloudflare var or secret as an integer. */
45
- export const integer = <const Name extends Key>(name: Name) => Config.int(name);
45
+ export const integer = <const Name extends Key>(name: Name) => Config.Int(name);
46
46
 
47
47
  /** Read a scalar Cloudflare var or secret as a boolean. */
48
- export const boolean = <const Name extends Key>(name: Name) => Config.boolean(name);
48
+ export const boolean = <const Name extends Key>(name: Name) => Config.Boolean(name);
49
49
 
50
50
  export interface ProviderOptions {
51
51
  /**
package/src/Worker.ts CHANGED
@@ -277,10 +277,17 @@ const renderFetchSuccess = <E, R, REvent, EventLayerError, EventLayerRequirement
277
277
  nativeResponse !== undefined
278
278
  ? Effect.void
279
279
  : Effect.map(Effect.context<never>(), (context) => {
280
- webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
281
- withoutBody: request.method === "HEAD",
282
- context,
283
- });
280
+ const withoutBody = request.method === "HEAD";
281
+
282
+ webResponse = HttpServerResponse.toWeb(
283
+ HttpServerResponse.omitsBody(response, withoutBody)
284
+ ? response
285
+ : HttpEffect.scopeTransferToStream(response),
286
+ {
287
+ withoutBody,
288
+ context,
289
+ },
290
+ );
284
291
  });
285
292
  const telemetryMiddleware = HttpMiddleware.make((self) =>
286
293
  RpcTargets.withScope(