effect-cf 0.22.0 → 0.23.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.
@@ -2,6 +2,8 @@ import { Cause, Context, Effect, Exit } from "effect";
2
2
 
3
3
  import { fromDurableObjectStorage, type DurableObjectStorage } from "./DurableObjectStorage";
4
4
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
5
+ import type { WorkerContextWaitUntilOptions } from "./Worker";
6
+ import { makeWaitUntilScheduler } from "./internal/WorkerContext";
5
7
 
6
8
  /**
7
9
  * Effect-friendly wrapper around Cloudflare `DurableObjectState`.
@@ -15,6 +17,16 @@ export interface DurableObjectStateService {
15
17
  readonly storage: DurableObjectStorage;
16
18
  /** Registers background work with Cloudflare's lifecycle. */
17
19
  waitUntil(promise: Promise<unknown>): Effect.Effect<void>;
20
+ /**
21
+ * Runs a background Effect via Cloudflare's `waitUntil` with the caller's
22
+ * Effect context, mirroring `WorkerContext.waitUntil` failure modes:
23
+ * `"observe"` (default) logs or routes failures to `onFailure`, while
24
+ * `"propagate"` also rejects the native `waitUntil` promise.
25
+ */
26
+ waitUntil<A, E, R, R2 = never>(
27
+ effect: Effect.Effect<A, E, R>,
28
+ options?: WorkerContextWaitUntilOptions<E, R2>,
29
+ ): Effect.Effect<void, never, R | R2>;
18
30
  /**
19
31
  * Runs an Effect inside Cloudflare's `blockConcurrencyWhile` gate.
20
32
  *
@@ -72,45 +84,60 @@ export class DurableObjectState extends Context.Service<
72
84
  */
73
85
  export const fromDurableObjectState = (
74
86
  state: globalThis.DurableObjectState,
75
- ): DurableObjectStateService => ({
76
- raw: state,
77
- id: state.id,
78
- storage: fromDurableObjectStorage(state.storage),
79
- waitUntil: (promise: Promise<unknown>) => Effect.sync(() => state.waitUntil(promise)),
80
- blockConcurrencyWhile: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
81
- Effect.context<R>().pipe(
82
- Effect.flatMap((context) =>
83
- Effect.promise(() =>
84
- state.blockConcurrencyWhile(() => runPromiseExitPreservingTypedFailures(context, effect)),
87
+ ): DurableObjectStateService => {
88
+ const scheduleWaitUntil = makeWaitUntilScheduler("DurableObjectState.waitUntil", (promise) =>
89
+ state.waitUntil(promise),
90
+ );
91
+ const waitUntil = (<A, E, R, R2 = never>(
92
+ input: Promise<unknown> | Effect.Effect<A, E, R>,
93
+ options?: WorkerContextWaitUntilOptions<E, R2>,
94
+ ) =>
95
+ Effect.isEffect(input)
96
+ ? scheduleWaitUntil(input, options, options?.mode ?? "observe")
97
+ : Effect.sync(() => state.waitUntil(input))) as DurableObjectStateService["waitUntil"];
98
+
99
+ return {
100
+ raw: state,
101
+ id: state.id,
102
+ storage: fromDurableObjectStorage(state.storage),
103
+ waitUntil,
104
+ blockConcurrencyWhile: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
105
+ Effect.context<R>().pipe(
106
+ Effect.flatMap((context) =>
107
+ Effect.promise(() =>
108
+ state.blockConcurrencyWhile(() =>
109
+ runPromiseExitPreservingTypedFailures(context, effect),
110
+ ),
111
+ ),
85
112
  ),
113
+ Effect.flatten,
86
114
  ),
87
- Effect.flatten,
88
- ),
89
- blockConcurrencyWhileOrReset: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
90
- Effect.context<R>().pipe(
91
- Effect.flatMap((context) =>
92
- Effect.promise(() =>
93
- state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect)),
115
+ blockConcurrencyWhileOrReset: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
116
+ Effect.context<R>().pipe(
117
+ Effect.flatMap((context) =>
118
+ Effect.promise(() =>
119
+ state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect)),
120
+ ),
94
121
  ),
95
122
  ),
123
+ acceptWebSocket: (ws: DurableWebSocket, tags?: Array<string>) =>
124
+ Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
125
+ getWebSockets: (tag?: string) =>
126
+ Effect.sync(() => state.getWebSockets(tag).map((socket) => fromWebSocket(socket))),
127
+ setWebSocketAutoResponse: (pair?: WebSocketRequestResponsePair) =>
128
+ Effect.sync(() => state.setWebSocketAutoResponse(pair)),
129
+ getWebSocketAutoResponse: Effect.sync(() => state.getWebSocketAutoResponse()),
130
+ getWebSocketAutoResponseTimestamp: (ws: DurableWebSocket) =>
131
+ Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws.raw)),
132
+ setHibernatableWebSocketEventTimeout: (timeoutMs?: number) =>
133
+ Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
134
+ getHibernatableWebSocketEventTimeout: Effect.sync(() =>
135
+ state.getHibernatableWebSocketEventTimeout(),
96
136
  ),
97
- acceptWebSocket: (ws: DurableWebSocket, tags?: Array<string>) =>
98
- Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
99
- getWebSockets: (tag?: string) =>
100
- Effect.sync(() => state.getWebSockets(tag).map((socket) => fromWebSocket(socket))),
101
- setWebSocketAutoResponse: (pair?: WebSocketRequestResponsePair) =>
102
- Effect.sync(() => state.setWebSocketAutoResponse(pair)),
103
- getWebSocketAutoResponse: Effect.sync(() => state.getWebSocketAutoResponse()),
104
- getWebSocketAutoResponseTimestamp: (ws: DurableWebSocket) =>
105
- Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws.raw)),
106
- setHibernatableWebSocketEventTimeout: (timeoutMs?: number) =>
107
- Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
108
- getHibernatableWebSocketEventTimeout: Effect.sync(() =>
109
- state.getHibernatableWebSocketEventTimeout(),
110
- ),
111
- getTags: (ws: DurableWebSocket) => Effect.sync(() => state.getTags(ws.raw)),
112
- abort: (reason?: string) => Effect.sync(() => state.abort(reason)),
113
- });
137
+ getTags: (ws: DurableWebSocket) => Effect.sync(() => state.getTags(ws.raw)),
138
+ abort: (reason?: string) => Effect.sync(() => state.abort(reason)),
139
+ };
140
+ };
114
141
 
115
142
  const runPromiseExitPreservingTypedFailures = async <A, E, R>(
116
143
  context: Context.Context<R>,
@@ -13,16 +13,14 @@ const causeError = <E>(cause: Cause.Cause<E>) => {
13
13
  };
14
14
 
15
15
  const failureHandler = <E, R>(
16
+ label: string,
16
17
  cause: Cause.Cause<E>,
17
18
  options: WorkerContextWaitUntilOptions<E, R> | undefined,
18
19
  ) =>
19
- (
20
- options?.onFailure?.(cause) ??
21
- Effect.logError("WorkerContext.waitUntil failed", Cause.pretty(cause))
22
- ).pipe(
20
+ (options?.onFailure?.(cause) ?? Effect.logError(`${label} failed`, Cause.pretty(cause))).pipe(
23
21
  Effect.catchCause((handlerCause) =>
24
22
  Effect.logError(
25
- "WorkerContext.waitUntil failure handler failed",
23
+ `${label} failure handler failed`,
26
24
  Cause.pretty(cause),
27
25
  Cause.pretty(handlerCause),
28
26
  ),
@@ -30,18 +28,19 @@ const failureHandler = <E, R>(
30
28
  );
31
29
 
32
30
  /**
33
- * Builds the `WorkerContext` service from a native `ExecutionContext`.
31
+ * Builds a `waitUntil` scheduler for any Cloudflare lifecycle that accepts
32
+ * background promises (`ExecutionContext`, `DurableObjectState`).
34
33
  *
35
34
  * Background effects capture the calling fiber's context via `Effect.context`
36
- * before being handed to `ctx.waitUntil`, so the default module-level
37
- * `Effect.runPromiseExit` runner is sufficient; entrypoints may still pass a
38
- * runtime-bound runner.
35
+ * before being registered, so the default module-level `Effect.runPromiseExit`
36
+ * runner is sufficient; entrypoints may still pass a runtime-bound runner.
39
37
  */
40
- export const fromExecutionContext = (
41
- ctx: globalThis.ExecutionContext,
38
+ export const makeWaitUntilScheduler = (
39
+ label: string,
40
+ register: (promise: Promise<unknown>) => void,
42
41
  runPromiseExit: RunWaitUntilEffect = (effect) => Effect.runPromiseExit(effect),
43
- ): WorkerContextService => {
44
- const schedule = <A, E, R, R2 = never>(
42
+ ) => {
43
+ return <A, E, R, R2 = never>(
45
44
  effect: Effect.Effect<A, E, R>,
46
45
  options: WorkerContextWaitUntilOptions<E, R2> | undefined,
47
46
  mode: "observe" | "propagate",
@@ -51,17 +50,14 @@ export const fromExecutionContext = (
51
50
  Effect.sync(() => {
52
51
  const runHandler = (cause: Cause.Cause<E>) =>
53
52
  runPromiseExit(
54
- Effect.scoped(Effect.provideContext(failureHandler(cause, options), context)),
53
+ Effect.scoped(Effect.provideContext(failureHandler(label, cause, options), context)),
55
54
  ).then((exit) => {
56
55
  if (Exit.isFailure(exit)) {
57
- console.error(
58
- "WorkerContext.waitUntil failure handler failed",
59
- Cause.pretty(exit.cause),
60
- );
56
+ console.error(`${label} failure handler failed`, Cause.pretty(exit.cause));
61
57
  }
62
58
  });
63
59
 
64
- ctx.waitUntil(
60
+ register(
65
61
  runPromiseExit(Effect.scoped(Effect.provideContext(effect, context))).then(
66
62
  async (exit) => {
67
63
  if (Exit.isSuccess(exit)) {
@@ -79,6 +75,20 @@ export const fromExecutionContext = (
79
75
  }),
80
76
  ),
81
77
  );
78
+ };
79
+
80
+ /**
81
+ * Builds the `WorkerContext` service from a native `ExecutionContext`.
82
+ */
83
+ export const fromExecutionContext = (
84
+ ctx: globalThis.ExecutionContext,
85
+ runPromiseExit: RunWaitUntilEffect = (effect) => Effect.runPromiseExit(effect),
86
+ ): WorkerContextService => {
87
+ const schedule = makeWaitUntilScheduler(
88
+ "WorkerContext.waitUntil",
89
+ (promise) => ctx.waitUntil(promise),
90
+ runPromiseExit,
91
+ );
82
92
 
83
93
  return {
84
94
  raw: ctx,