effect-cf 0.42.0 → 0.43.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.43.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+
9
+ Require Effect `^4.0.0-rc.115` and matching `@effect/sql-d1`, `@effect/sql-pg`, and
10
+ `@effect/sql-sqlite-do` peers. Upgrade the Effect family together.
11
+
12
+ Preserve Cloudflare span context with Effect's new fiber representation and close
13
+ request scopes when HEAD or status 204, 205, or 304 omits an Effect streaming body.
14
+ Omitted streams are not started, and request resources and telemetry are finalized.
15
+
16
+ HyperdrivePg now uses Effect's native PostgreSQL driver. Consumers must account for
17
+ its new result codecs (`int8` becomes `bigint`, dates become strings, and timestamps
18
+ become epoch milliseconds), wrap JSON parameters with `sql.json`, and submit one
19
+ statement per query. The driver enables named prepared statements by default;
20
+ pass `prepare: false` for poolers that cannot preserve them.
21
+
22
+ ## effect-webtransport
23
+
24
+ Require Effect `^4.0.0-rc.115` and migrate the Socket adapter to scoped readers and
25
+ writers. Replace `socket.run(handler)` with a scoped acquisition of `socket.reader`
26
+ and a loop over `reader.pull`. Use `writer.write` and `writer.writeAll` after
27
+ acquiring `socket.writer`.
28
+
29
+ Reads now follow consumer demand without dispatching a fiber per chunk. Reader
30
+ scope cleanup still releases stream locks, sends FIN on success, and aborts writes
31
+ on failure. Releasing a writer scope half-closes the write side. Interrupting a
32
+ batch stops unsent frames while leaving the socket available for later writes.
33
+
34
+ Every close, including peer FIN, fails with a `SocketError` containing
35
+ `SocketCloseError`. Remove `closeCodeIsError`, `FromBidirectionalStreamOptions`,
36
+ and `defaultCloseCodeIsError`; handle close reasons in the read loop and use
37
+ `Effect.retry` around the scoped loop to reconnect. WebSocket fallback options now
38
+ accept `highWaterMark`.
39
+
40
+ MessagePack serialization was removed upstream. Use `RpcSerialization.layerNdjson`
41
+ or `RpcSerialization.layerSchemaBinary()` for the byte-stream transport.
42
+
43
+ ## 0.42.1
44
+
45
+ ### Patch Changes
46
+
47
+ - [#171](https://github.com/danieljvdm/effect-cf/pull/171) [`06a0bfc`](https://github.com/danieljvdm/effect-cf/commit/06a0bfc1f750f5042e13f97b212c63ced5caa5e5) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Prevent the first Durable Object event from overtaking an initialization hook's concurrency gate while its service layer is still building. Hooks that perform background setup without a concurrency gate continue to allow incoming events.
48
+
49
+ - [#170](https://github.com/danieljvdm/effect-cf/pull/170) [`4c41e8d`](https://github.com/danieljvdm/effect-cf/commit/4c41e8d648f340dbf43c5e2c17a850d967b7fa09) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Prevent Durable Object storage transactions and concurrency-blocking callbacks from stalling when their Effect work yields while an earlier timer is blocked by the native input gate.
50
+
3
51
  ## 0.42.0
4
52
 
5
53
  ### Minor 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.
@@ -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));
@@ -61,7 +61,7 @@ const layer = Layer.effect(Tracer.Tracer, Effect.sync(() => {
61
61
  return parentContext(() => tracing.startActiveSpan(options.name, (span) => new Span(options, AsyncLocalStorage.snapshot(), span)));
62
62
  },
63
63
  context(primitive, fiber) {
64
- return contextFor(fiber.currentSpan)(() => primitive["~effect/Effect/evaluate"](fiber));
64
+ return contextFor(fiber.cache.span)(() => primitive["~effect/Effect/evaluate"](fiber));
65
65
  }
66
66
  });
67
67
  }));
@@ -47,8 +47,10 @@ interface DurableObjectOptionsBase<RRuntime, REvent = never, EventLayerError = n
47
47
  /**
48
48
  * Effect run when Cloudflare loads this Durable Object instance into memory.
49
49
  *
50
- * Use `DurableObjectState.blockConcurrencyWhile` inside this hook when
51
- * incoming events should wait for setup to finish. Cloudflare may construct
50
+ * The base layer is acquired before incoming events are admitted. The hook
51
+ * itself runs in the background; start it with
52
+ * `DurableObjectState.blockConcurrencyWhile` when incoming events should
53
+ * wait for setup to finish. Cloudflare may construct
52
54
  * the same Durable Object id again after eviction or restart; use Durable
53
55
  * Object storage if work must happen only once per id.
54
56
  */
@@ -10,6 +10,7 @@ import { DurableObjectAlarm } from "./DurableObjectAlarm.mjs";
10
10
  import { Tag, implement, method, native } from "./DurableObjectDefinition.mjs";
11
11
  import * as Effect from "effect/Effect";
12
12
  import * as Layer from "effect/Layer";
13
+ import * as Scheduler from "effect/Scheduler";
13
14
  import { DurableObject } from "cloudflare:workers";
14
15
  import * as Tracer from "effect/Tracer";
15
16
  //#region src/DurableObject.ts
@@ -36,7 +37,10 @@ function make(layer, options = {}) {
36
37
  const services = (registration?.layer ?? Layer.empty).pipe(Layer.provideMerge(baseServices));
37
38
  this.runtime = makeEntrypointRuntime(layer, env, services);
38
39
  const initialize = options.initialize;
39
- if (initialize !== void 0) state.waitUntil(this[RunSymbol](initialize, { eventLayer: false }));
40
+ if (initialize !== void 0) state.waitUntil(state.blockConcurrencyWhile(async () => {
41
+ await Effect.runPromise(this.runtime.contextEffect, { scheduler: new Scheduler.MixedScheduler("sync") });
42
+ state.waitUntil(this[RunSymbol](initialize, { eventLayer: false }));
43
+ }));
40
44
  }
41
45
  [RunSymbol](effect, runOptions = {}) {
42
46
  const eventLayer = runOptions.eventLayer === false ? void 0 : options.eventLayer;
@@ -3,8 +3,8 @@ import { DurableObjectState } from "./DurableObjectState.mjs";
3
3
  import * as Context from "effect/Context";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Layer from "effect/Layer";
6
- import { SqliteClient } from "@effect/sql-sqlite-do";
7
6
  import * as Scheduler from "effect/Scheduler";
7
+ import { SqliteClient } from "@effect/sql-sqlite-do";
8
8
  import * as Reactivity from "effect/unstable/reactivity/Reactivity";
9
9
  import { SqlClient } from "effect/unstable/sql";
10
10
  //#region src/DurableObjectSqlite.ts
@@ -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
  });
@@ -1,7 +1,10 @@
1
+ import * as Context from "effect/Context";
1
2
  import * as Effect from "effect/Effect";
2
3
  import "effect/Exit";
3
4
  import * as FiberSet from "effect/FiberSet";
5
+ import * as Scheduler from "effect/Scheduler";
4
6
  //#region src/internal/NativeCallback.ts
7
+ const callbackScheduler = new Scheduler.MixedScheduler("sync");
5
8
  /**
6
9
  * Runs an Effect callback owned by a native Promise operation.
7
10
  *
@@ -10,7 +13,7 @@ import * as FiberSet from "effect/FiberSet";
10
13
  * the native operation's settlement.
11
14
  */
12
15
  const runNativeCallback = Effect.fnUntraced(function* (operation) {
13
- const callerContext = yield* Effect.context();
16
+ const callbackContext = Context.add(yield* Effect.context(), Scheduler.Scheduler, callbackScheduler);
14
17
  return yield* Effect.scoped(Effect.gen(function* () {
15
18
  let nativePromise;
16
19
  yield* Effect.addFinalizer(() => {
@@ -20,7 +23,7 @@ const runNativeCallback = Effect.fnUntraced(function* (operation) {
20
23
  const runPromise = yield* FiberSet.makeRuntimePromise();
21
24
  return yield* Effect.callback((resume) => {
22
25
  try {
23
- nativePromise = operation((effect) => runPromise(Effect.provideContext(Effect.exit(effect), callerContext)));
26
+ nativePromise = operation((effect) => runPromise(Effect.provideContext(Effect.exit(effect), callbackContext)));
24
27
  } catch (cause) {
25
28
  resume(Effect.fail(cause));
26
29
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.42.0",
3
+ "version": "0.43.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>(
@@ -91,7 +91,7 @@ export const layer: Layer.Layer<never> = Layer.effect(
91
91
  );
92
92
  },
93
93
  context(primitive, fiber) {
94
- return contextFor(fiber.currentSpan)(() => primitive["~effect/Effect/evaluate"](fiber));
94
+ return contextFor(fiber.cache.span)(() => primitive["~effect/Effect/evaluate"](fiber));
95
95
  },
96
96
  });
97
97
  }),
@@ -1,6 +1,7 @@
1
1
  import { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Layer from "effect/Layer";
4
+ import * as Scheduler from "effect/Scheduler";
4
5
  import * as Tracer from "effect/Tracer";
5
6
  import type { ManagedRuntime, Scope } from "effect";
6
7
 
@@ -125,8 +126,10 @@ interface DurableObjectOptionsBase<
125
126
  /**
126
127
  * Effect run when Cloudflare loads this Durable Object instance into memory.
127
128
  *
128
- * Use `DurableObjectState.blockConcurrencyWhile` inside this hook when
129
- * incoming events should wait for setup to finish. Cloudflare may construct
129
+ * The base layer is acquired before incoming events are admitted. The hook
130
+ * itself runs in the background; start it with
131
+ * `DurableObjectState.blockConcurrencyWhile` when incoming events should
132
+ * wait for setup to finish. Cloudflare may construct
130
133
  * the same Durable Object id again after eviction or restart; use Durable
131
134
  * Object storage if work must happen only once per id.
132
135
  */
@@ -298,7 +301,17 @@ export function make<
298
301
  const initialize = options.initialize;
299
302
 
300
303
  if (initialize !== undefined) {
301
- state.waitUntil(this[RunSymbol](initialize, { eventLayer: false }));
304
+ state.waitUntil(
305
+ state.blockConcurrencyWhile(async () => {
306
+ // Build services before admitting the first event, then let the
307
+ // hook synchronously install its own gate if it needs one. Its
308
+ // background work and failure reporting keep their waitUntil owner.
309
+ await Effect.runPromise(this.runtime.contextEffect, {
310
+ scheduler: new Scheduler.MixedScheduler("sync"),
311
+ });
312
+ state.waitUntil(this[RunSymbol](initialize, { eventLayer: false }));
313
+ }),
314
+ );
302
315
  }
303
316
  }
304
317
 
@@ -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(
@@ -1,9 +1,13 @@
1
+ import * as Context from "effect/Context";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Exit from "effect/Exit";
3
4
  import * as FiberSet from "effect/FiberSet";
5
+ import * as Scheduler from "effect/Scheduler";
4
6
 
5
7
  type RunCallback<A, E, R> = (effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, E>>;
6
8
 
9
+ const callbackScheduler = new Scheduler.MixedScheduler("sync");
10
+
7
11
  /**
8
12
  * Runs an Effect callback owned by a native Promise operation.
9
13
  *
@@ -14,7 +18,14 @@ type RunCallback<A, E, R> = (effect: Effect.Effect<A, E, R>) => Promise<Exit.Exi
14
18
  export const runNativeCallback = Effect.fnUntraced(function* <A, E, R, B>(
15
19
  operation: (run: RunCallback<A, E, R>) => Promise<B>,
16
20
  ): Effect.fn.Return<B, unknown, R> {
17
- const callerContext = yield* Effect.context<R>();
21
+ // An older timer in the blocked parent input gate prevents later timers in
22
+ // this owner from running. Yield through microtasks while the native gate is
23
+ // held, including when the caller explicitly provided its own scheduler.
24
+ const callbackContext = Context.add(
25
+ yield* Effect.context<R>(),
26
+ Scheduler.Scheduler,
27
+ callbackScheduler,
28
+ );
18
29
 
19
30
  return yield* Effect.scoped(
20
31
  Effect.gen(function* () {
@@ -39,7 +50,7 @@ export const runNativeCallback = Effect.fnUntraced(function* <A, E, R, B>(
39
50
  return yield* Effect.callback<B, unknown>((resume) => {
40
51
  try {
41
52
  nativePromise = operation((effect) =>
42
- runPromise(Effect.provideContext(Effect.exit(effect), callerContext)),
53
+ runPromise(Effect.provideContext(Effect.exit(effect), callbackContext)),
43
54
  );
44
55
  } catch (cause) {
45
56
  resume(Effect.fail(cause));