effect-cf 0.42.0 → 0.42.1

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,13 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.42.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ - [#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.
10
+
3
11
  ## 0.42.0
4
12
 
5
13
  ### Minor Changes
@@ -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
@@ -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.42.1",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
 
@@ -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));