effect-cf 0.53.0 → 0.54.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.
@@ -31,14 +31,16 @@ const makeEntrypointRuntime = (layer, env, services) => {
31
31
  const entrypointServices = Layer.mergeAll(services, ConfigProvider.layer(WorkerConfig.providerFromEnv(env)), Layer.succeed(WorkerEnvironment, env));
32
32
  return ManagedRuntime.make(provideEntrypointServices(layer, entrypointServices));
33
33
  };
34
+ /** Close event resources before an outer lifecycle (such as alarm reconciliation) completes. */
35
+ const scopeEvent = (effect) => Effect.scoped(withScope(effect));
34
36
  function runEventPromise(...args) {
35
37
  const [runtime, effect, eventLayer, parent, onFailure] = args;
36
38
  if (eventLayer === void 0) {
37
- const event = Effect.scoped(withScope(effect));
39
+ const event = scopeEvent(effect);
38
40
  return runPromise(runtime, parent === void 0 ? event : Effect.withParentSpan(event, parent), onFailure);
39
41
  }
40
- const event = Effect.scoped(withScope(effect.pipe(Effect.provide(eventLayer, { local: true }))));
42
+ const event = scopeEvent(effect.pipe(Effect.provide(eventLayer, { local: true })));
41
43
  return runPromise(runtime, parent === void 0 ? event : Effect.withParentSpan(event, parent), onFailure);
42
44
  }
43
45
  //#endregion
44
- export { makeEntrypointRuntime, runEventPromise };
46
+ export { makeEntrypointRuntime, runEventPromise, scopeEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,8 +10,14 @@ import { WorkerEnvironment, type WorkerEnv } from "./Environment";
10
10
  import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState";
11
11
  import {
12
12
  DurableObjectAlarm,
13
+ DispatchAfterEvent,
14
+ HasDueAlarms,
15
+ RunAlarm,
16
+ dispatchRawAlarm,
13
17
  type AlarmRegistration,
14
18
  type AlarmService,
19
+ type InvalidAlarmRegistrationError,
20
+ type InvalidScheduleConfigurationError,
15
21
  } from "./DurableObjectAlarm";
16
22
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
17
23
  import * as RpcDefinition from "./RpcDefinition";
@@ -229,6 +235,7 @@ export type DurableObjectClass<Rpc extends DurableObjectRpc<ROut>, ROut> = new (
229
235
  env: WorkerEnv,
230
236
  ) => CloudflareDurableObject<WorkerEnv> &
231
237
  DurableObjectRpcApi<Rpc, ROut> & {
238
+ alarm(alarmInfo?: globalThis.AlarmInvocationInfo): Promise<void> | void;
232
239
  [RunSymbol]<A, E>(
233
240
  effect: Effect.Effect<A, E, HandlerContext<ROut>>,
234
241
  options?: RunOptions,
@@ -274,12 +281,15 @@ export function make<
274
281
  ): DurableObjectClass<Rpc, ROut | REvent | RAlarm> {
275
282
  const registration =
276
283
  options.alarms !== undefined && !Effect.isEffect(options.alarms) ? options.alarms : undefined;
277
- const logicalAlarms = (
278
- registration?.run ?? (Effect.isEffect(options.alarms) ? options.alarms : undefined)
279
- )?.pipe(Effect.asVoid);
284
+ const legacyAlarms = Effect.isEffect(options.alarms)
285
+ ? options.alarms.pipe(Effect.asVoid)
286
+ : undefined;
280
287
 
281
288
  class EffectDurableObject extends CloudflareDurableObject<WorkerEnv> {
282
- readonly runtime: ManagedRuntime.ManagedRuntime<RuntimeContext<ROut | RAlarm>, LayerError>;
289
+ readonly runtime: ManagedRuntime.ManagedRuntime<
290
+ RuntimeContext<ROut | RAlarm>,
291
+ LayerError | InvalidAlarmRegistrationError
292
+ >;
283
293
 
284
294
  constructor(state: globalThis.DurableObjectState, env: WorkerEnv) {
285
295
  super(state, env);
@@ -290,12 +300,16 @@ export function make<
290
300
  // SAFETY: RAlarm is inferred only from the registration's layer; without a registration it is never.
291
301
  const services = (registration?.layer ?? Layer.empty).pipe(
292
302
  Layer.provideMerge(baseServices),
293
- ) as Layer.Layer<DurableObjectState | DurableObjectAlarm | RAlarm>;
303
+ ) as Layer.Layer<
304
+ DurableObjectState | DurableObjectAlarm | RAlarm,
305
+ InvalidAlarmRegistrationError
306
+ >;
294
307
 
295
308
  this.runtime = Runtime.makeEntrypointRuntime<
296
309
  ROut,
297
310
  LayerError,
298
- DurableObjectState | DurableObjectAlarm | RAlarm
311
+ DurableObjectState | DurableObjectAlarm | RAlarm,
312
+ InvalidAlarmRegistrationError
299
313
  >(layer, env, services);
300
314
 
301
315
  const initialize = options.initialize;
@@ -322,27 +336,54 @@ export function make<
322
336
  const eventLayer = runOptions.eventLayer === false ? undefined : options.eventLayer;
323
337
  const parent = runOptions.rpc?.parent;
324
338
  const parentSpan = parent === undefined ? undefined : Tracer.externalSpan(parent);
339
+ const afterResponse = runOptions.event === "rpc" || runOptions.event === "fetch";
340
+ let dispatchAfterEvent = false;
341
+ const handler = afterResponse
342
+ ? effect.pipe(
343
+ Effect.onExit(() =>
344
+ Effect.flatMap(DurableObjectAlarm, (alarms) =>
345
+ Effect.map(alarms[DispatchAfterEvent], (enabled) => {
346
+ dispatchAfterEvent = enabled;
347
+ }),
348
+ ),
349
+ ),
350
+ )
351
+ : effect;
352
+ let response: Promise<A>;
325
353
 
326
354
  if (eventLayer === undefined) {
327
355
  // SAFETY: initialize cannot require event services; without an event layer, all requirements are already provided.
328
- return Runtime.runEventPromise(
356
+ response = Runtime.runEventPromise(
329
357
  this.runtime,
330
358
  // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
331
- effect as Effect.Effect<A, E, HandlerContext<ROut | RAlarm>>,
359
+ handler as Effect.Effect<A, E, HandlerContext<ROut | RAlarm>>,
332
360
  undefined,
333
361
  parentSpan,
334
362
  runOptions.onFailure,
335
363
  );
364
+ } else {
365
+ response = Runtime.runEventPromise<
366
+ A,
367
+ E,
368
+ RuntimeContext<ROut | RAlarm>,
369
+ REvent,
370
+ EventLayerError,
371
+ LayerError | InvalidAlarmRegistrationError
372
+ >(this.runtime, handler, eventLayer, parentSpan, runOptions.onFailure);
336
373
  }
337
374
 
338
- return Runtime.runEventPromise<
339
- A,
340
- E,
341
- RuntimeContext<ROut | RAlarm>,
342
- REvent,
343
- EventLayerError,
344
- LayerError
345
- >(this.runtime, effect, eventLayer, parentSpan, runOptions.onFailure);
375
+ if (!afterResponse) return response;
376
+
377
+ return response.finally(() => {
378
+ if (dispatchAfterEvent) {
379
+ this.ctx.waitUntil(
380
+ // The default scheduler yields to a later task, after the response chain unwinds.
381
+ Effect.runPromise(
382
+ Effect.yieldNow.pipe(Effect.andThen(Effect.promise(() => this.#runAlarm(false)))),
383
+ ),
384
+ );
385
+ }
386
+ });
346
387
  }
347
388
 
348
389
  fetch(request: Request): Promise<Response> {
@@ -357,21 +398,43 @@ export function make<
357
398
  });
358
399
  }
359
400
 
360
- alarm(alarmInfo?: globalThis.AlarmInvocationInfo): Promise<void> | void {
361
- const rawAlarm = options.alarm?.(alarmInfo);
362
- const alarmEffect =
363
- logicalAlarms !== undefined && rawAlarm !== undefined
364
- ? Effect.gen(function* () {
365
- yield* logicalAlarms;
366
- yield* rawAlarm;
367
- })
368
- : (logicalAlarms ?? rawAlarm);
369
-
370
- if (alarmEffect !== undefined) {
371
- return this[RunSymbol](alarmEffect.pipe(Effect.onExit(() => scheduleTelemetryFlush)), {
401
+ alarm(alarmInfo?: globalThis.AlarmInvocationInfo): Promise<void> {
402
+ return this.#runAlarm(true, alarmInfo);
403
+ }
404
+
405
+ #runAlarm(native: boolean, alarmInfo?: globalThis.AlarmInvocationInfo): Promise<void> {
406
+ const event = (beginDispatch: Effect.Effect<void, InvalidScheduleConfigurationError>) => {
407
+ const alarmEffect = Effect.gen(function* () {
408
+ const alarms = yield* DurableObjectAlarm;
409
+
410
+ if (!native && !(yield* alarms[HasDueAlarms])) return;
411
+ const rawAlarm = options.alarm?.(alarmInfo);
412
+ const customAlarm =
413
+ legacyAlarms === undefined
414
+ ? (rawAlarm ?? Effect.void)
415
+ : legacyAlarms.pipe(Effect.andThen(rawAlarm ?? Effect.void));
416
+
417
+ yield* beginDispatch;
418
+ // Custom dispatchers, including Effect-form alarms, recover retained checkpoints.
419
+ yield* registration === undefined || rawAlarm !== undefined
420
+ ? dispatchRawAlarm(customAlarm, registration)
421
+ : registration.run.pipe(Effect.asVoid);
422
+ }).pipe(Effect.onExit(() => scheduleTelemetryFlush));
423
+
424
+ return Runtime.scopeEvent(
425
+ options.eventLayer === undefined
426
+ ? alarmEffect
427
+ : alarmEffect.pipe(Effect.provide(options.eventLayer, { local: true })),
428
+ );
429
+ };
430
+
431
+ return this[RunSymbol](
432
+ Effect.flatMap(DurableObjectAlarm, (alarms) => alarms[RunAlarm](event, native)),
433
+ {
372
434
  event: "alarm",
373
- });
374
- }
435
+ eventLayer: false,
436
+ },
437
+ );
375
438
  }
376
439
 
377
440
  webSocketMessage(socket: WebSocket, message: string | ArrayBuffer): Promise<void> | void {