effect-cf 0.50.0 → 0.52.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.
@@ -102,6 +102,10 @@ export interface ContainerTcpPort {
102
102
  readonly connect: (
103
103
  ...args: Parameters<globalThis.Fetcher["connect"]>
104
104
  ) => Effect.Effect<ReturnType<globalThis.Fetcher["connect"]>, ContainerError>;
105
+ /** Closes the socket when the scope ends, awaiting cleanup and preserving close failures as defects. */
106
+ readonly connectScoped: (
107
+ ...args: Parameters<globalThis.Fetcher["connect"]>
108
+ ) => Effect.Effect<ReturnType<globalThis.Fetcher["connect"]>, ContainerError, Scope.Scope>;
105
109
  }
106
110
 
107
111
  /** Direct container control inside a Durable Object; lifecycle policy belongs to the application. */
@@ -248,6 +252,9 @@ const fromTcpPort = (port: globalThis.Fetcher): ContainerTcpPort => {
248
252
  });
249
253
  });
250
254
 
255
+ const connect: ContainerTcpPort["connect"] = (...args) =>
256
+ attempt("getTcpPort.connect", () => port.connect(...args));
257
+
251
258
  return {
252
259
  raw: port,
253
260
  fetch,
@@ -259,7 +266,11 @@ const fromTcpPort = (port: globalThis.Fetcher): ContainerTcpPort => {
259
266
 
260
267
  return new Request(url, request);
261
268
  }).pipe(Effect.flatMap((forwarded) => fetch(forwarded))),
262
- connect: (...args) => attempt("getTcpPort.connect", () => port.connect(...args)),
269
+ connect,
270
+ connectScoped: (...args) =>
271
+ Effect.acquireRelease(connect(...args), (socket) =>
272
+ attemptPromise("getTcpPort.close", () => socket.close()).pipe(Effect.orDie),
273
+ ),
263
274
  };
264
275
  };
265
276
 
@@ -87,8 +87,8 @@ export interface LayerOptions {
87
87
  readonly attachmentKey?: string | undefined;
88
88
  /**
89
89
  * Heartbeat behavior. `"auto-response"` (the default) installs Cloudflare's
90
- * hibernation-safe text Ping/Pong response while an RPC socket exists and no
91
- * non-resumable RPC operation is active, then removes it while one is pending.
90
+ * hibernation-safe text Ping/Pong response for the lifetime of RPC sockets,
91
+ * including pending operations. Keepalives never dispatch application code.
92
92
  * `"passthrough"` leaves the Durable Object's existing auto-response
93
93
  * configuration unchanged and makes the application responsible for waking a
94
94
  * lost pending operation.
@@ -248,7 +248,6 @@ export const layer = (
248
248
  readonly request: RpcMessage.RequestEncoded;
249
249
  }> = [];
250
250
  let nextClientId = 0;
251
- let activeNonResumableRequestCount = 0;
252
251
  let writeRequest:
253
252
  | ((clientId: number, data: RpcMessage.FromClientEncoded) => Effect.Effect<void>)
254
253
  | undefined;
@@ -383,8 +382,7 @@ export const layer = (
383
382
  heartbeatPolicy,
384
383
  );
385
384
 
386
- const syncHeartbeat = () =>
387
- heartbeat.setEnabled(connectionsBySocket.size > 0 && activeNonResumableRequestCount === 0);
385
+ const syncHeartbeat = () => heartbeat.setEnabled(connectionsBySocket.size > 0);
388
386
 
389
387
  const persistConnection = Effect.fn("DurableObjectRpcWebSocket.persistConnection")(function* (
390
388
  connection: RpcConnection,
@@ -430,7 +428,6 @@ export const layer = (
430
428
 
431
429
  connectionsBySocket.delete(socket.raw);
432
430
  connectionsById.delete(connection.id);
433
- activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
434
431
  connection.requestIds.clear();
435
432
  connection.nonResumableRequestIds.clear();
436
433
  connection.subscriptionsByRequestId.clear();
@@ -511,7 +508,6 @@ export const layer = (
511
508
  } else {
512
509
  connection.requestIds.add(request.id);
513
510
  connection.nonResumableRequestIds.add(request.id);
514
- activeNonResumableRequestCount++;
515
511
  }
516
512
 
517
513
  if (!(yield* persistOrReset(connection))) {
@@ -529,9 +525,7 @@ export const layer = (
529
525
  preserveAckFilter = false,
530
526
  ) {
531
527
  if (connection.requestIds.delete(requestId)) {
532
- if (connection.nonResumableRequestIds.delete(requestId)) {
533
- activeNonResumableRequestCount--;
534
- }
528
+ connection.nonResumableRequestIds.delete(requestId);
535
529
 
536
530
  const subscription = removeSubscription(connection, requestId);
537
531
 
@@ -557,10 +551,6 @@ export const layer = (
557
551
  function* (connection: RpcConnection) {
558
552
  const hadRequests = connection.requestIds.size > 0;
559
553
 
560
- if (connection.requestIds.size > 0) {
561
- activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
562
- }
563
-
564
554
  connection.requestIds.clear();
565
555
  connection.nonResumableRequestIds.clear();
566
556
  connection.subscriptionsByRequestId.clear();
@@ -1,7 +1,5 @@
1
1
  import * as Cause from "effect/Cause";
2
- import * as Clock from "effect/Clock";
3
2
  import * as Data from "effect/Data";
4
- import * as Duration from "effect/Duration";
5
3
  import * as Effect from "effect/Effect";
6
4
  import * as Exit from "effect/Exit";
7
5
  import * as Fiber from "effect/Fiber";
@@ -91,15 +89,6 @@ export interface DurableObjectTransaction {
91
89
  delete(keys: Array<string>, options?: globalThis.DurableObjectPutOptions): StorageEffect<number>;
92
90
  rollback(): StorageEffect<void>;
93
91
  getAlarm(options?: globalThis.DurableObjectGetAlarmOptions): StorageEffect<number | null>;
94
- setAlarm(
95
- scheduledTime: number | Date,
96
- options?: globalThis.DurableObjectSetAlarmOptions,
97
- ): StorageEffect<void>;
98
- setAlarmAfter(
99
- delay: Duration.Input,
100
- options?: globalThis.DurableObjectSetAlarmOptions,
101
- ): StorageEffect<void>;
102
- deleteAlarm(options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
103
92
  }
104
93
 
105
94
  export interface DurableObjectStorage {
@@ -110,20 +99,11 @@ export interface DurableObjectStorage {
110
99
  put<T>(key: string, value: T, options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
111
100
  delete(key: string, options?: globalThis.DurableObjectPutOptions): StorageEffect<boolean>;
112
101
  /**
113
- * Deletes all stored data. On compatibility dates before 2026-02-24, Cloudflare
114
- * documents that active alarms must be deleted separately with `deleteAlarm()`.
102
+ * Deletes all stored data, including logical schedules. Use the alarm scheduler
103
+ * to cancel individual alarms without deleting application data.
115
104
  */
116
105
  deleteAll(options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
117
106
  getAlarm(options?: globalThis.DurableObjectGetAlarmOptions): StorageEffect<number | null>;
118
- setAlarm(
119
- scheduledTime: number | Date,
120
- options?: globalThis.DurableObjectSetAlarmOptions,
121
- ): StorageEffect<void>;
122
- setAlarmAfter(
123
- delay: Duration.Input,
124
- options?: globalThis.DurableObjectSetAlarmOptions,
125
- ): StorageEffect<void>;
126
- deleteAlarm(options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
127
107
  /**
128
108
  * SQLite-backed Durable Object storage only.
129
109
  *
@@ -165,21 +145,6 @@ const tryStoragePromise = <A>(operation: string, evaluate: () => Promise<A>): St
165
145
  catch: (cause) => storageError(operation, cause),
166
146
  });
167
147
 
168
- const setAlarmAfter = Effect.fn("DurableObjectStorage.setAlarmAfter")(function* (
169
- operation: string,
170
- setAlarm: (
171
- scheduledTime: number,
172
- options?: globalThis.DurableObjectSetAlarmOptions,
173
- ) => Promise<void>,
174
- delay: Duration.Input,
175
- options?: globalThis.DurableObjectSetAlarmOptions,
176
- ) {
177
- const now = yield* Clock.currentTimeMillis;
178
- const delayMillis = Math.ceil(Duration.toMillis(delay));
179
-
180
- yield* tryStoragePromise(operation, () => setAlarm(now + delayMillis, options));
181
- });
182
-
183
148
  const fromSqlCursor = <T extends Record<string, SqlStorageValue>>(
184
149
  cursor: globalThis.SqlStorageCursor<T>,
185
150
  ): SqlCursor<T> => ({
@@ -297,17 +262,6 @@ const fromDurableObjectTransaction = (
297
262
  rollback: () => tryStorageSync("transaction.rollback", () => txn.rollback()),
298
263
  getAlarm: (options?: globalThis.DurableObjectGetAlarmOptions) =>
299
264
  tryStoragePromise("transaction.getAlarm", () => txn.getAlarm(options)),
300
- setAlarm: (scheduledTime: number | Date, options?: globalThis.DurableObjectSetAlarmOptions) =>
301
- tryStoragePromise("transaction.setAlarm", () => txn.setAlarm(scheduledTime, options)),
302
- setAlarmAfter: (delay: Duration.Input, options?: globalThis.DurableObjectSetAlarmOptions) =>
303
- setAlarmAfter(
304
- "transaction.setAlarm",
305
- (scheduledTime, options) => txn.setAlarm(scheduledTime, options),
306
- delay,
307
- options,
308
- ),
309
- deleteAlarm: (options?: globalThis.DurableObjectSetAlarmOptions) =>
310
- tryStoragePromise("transaction.deleteAlarm", () => txn.deleteAlarm(options)),
311
265
  }) as DurableObjectTransaction;
312
266
 
313
267
  export const fromDurableObjectStorage = (
@@ -323,17 +277,6 @@ export const fromDurableObjectStorage = (
323
277
  tryStoragePromise("deleteAll", () => storage.deleteAll(options)),
324
278
  getAlarm: (options?: globalThis.DurableObjectGetAlarmOptions) =>
325
279
  tryStoragePromise("getAlarm", () => storage.getAlarm(options)),
326
- setAlarm: (scheduledTime: number | Date, options?: globalThis.DurableObjectSetAlarmOptions) =>
327
- tryStoragePromise("setAlarm", () => storage.setAlarm(scheduledTime, options)),
328
- setAlarmAfter: (delay: Duration.Input, options?: globalThis.DurableObjectSetAlarmOptions) =>
329
- setAlarmAfter(
330
- "setAlarm",
331
- (scheduledTime, options) => storage.setAlarm(scheduledTime, options),
332
- delay,
333
- options,
334
- ),
335
- deleteAlarm: (options?: globalThis.DurableObjectSetAlarmOptions) =>
336
- tryStoragePromise("deleteAlarm", () => storage.deleteAlarm(options)),
337
280
  transactionSync: <A, E, R>(closure: () => Effect.Effect<A, E, R>) =>
338
281
  Effect.context<R>().pipe(
339
282
  Effect.flatMap((context) =>
@@ -7,6 +7,30 @@ import * as S from "effect/Schema";
7
7
  import { DurableObjectState } from "./DurableObjectState";
8
8
  import * as ErrorMessage from "./internal/ErrorMessage";
9
9
 
10
+ export const DEFAULT_KEEPALIVE_REQUEST = "effect-cf:ping";
11
+ export const DEFAULT_KEEPALIVE_RESPONSE = "effect-cf:pong";
12
+
13
+ /**
14
+ * Registers exact text keepalives with Cloudflare so they never invoke the object.
15
+ * The pair applies to all hibernating sockets on this object; install in its constructor.
16
+ * Clients send the request text and consume the response outside their application protocol.
17
+ */
18
+ export const installKeepalive = Effect.fn("DurableObjectWebSocket.installKeepalive")(function* (
19
+ request = DEFAULT_KEEPALIVE_REQUEST,
20
+ response = DEFAULT_KEEPALIVE_RESPONSE,
21
+ ) {
22
+ const state = yield* DurableObjectState;
23
+ const configured = yield* state.getWebSocketAutoResponse;
24
+
25
+ if (configured !== null && (configured.request !== request || configured.response !== response)) {
26
+ return yield* Effect.die(
27
+ new Error("Durable Object already has a different keepalive protocol"),
28
+ );
29
+ }
30
+
31
+ yield* state.setWebSocketAutoResponse(new WebSocketRequestResponsePair(request, response));
32
+ });
33
+
10
34
  export type DurableWebSocketSendData = string | ArrayBuffer | ArrayBufferView;
11
35
 
12
36
  export class DurableWebSocketSendError extends Data.TaggedError("DurableWebSocketSendError")<{