effect-cf 0.49.0 → 0.51.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.
@@ -0,0 +1,388 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Data from "effect/Data";
3
+ import * as Duration from "effect/Duration";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Option from "effect/Option";
7
+ import * as Stream from "effect/Stream";
8
+ import type { Scope } from "effect";
9
+
10
+ import { DurableObjectState } from "./DurableObjectState";
11
+ import * as ErrorMessage from "./internal/ErrorMessage";
12
+
13
+ export type ContainerStartOptions = globalThis.ContainerStartupOptions;
14
+ export type ContainerExecOptions = Omit<globalThis.ContainerExecOptions, "signal">;
15
+ export type ContainerInfo = globalThis.ContainerInfo;
16
+ export type ContainerSnapshot = globalThis.ContainerSnapshot;
17
+ export type ContainerSnapshotOptions = globalThis.ContainerSnapshotOptions;
18
+ export type ContainerExecOutput = globalThis.ExecOutput;
19
+
20
+ export interface ContainerExecOutputText {
21
+ readonly stdout: string;
22
+ readonly stderr: string;
23
+ readonly exitCode: number;
24
+ }
25
+
26
+ export interface ContainerProcessLog {
27
+ readonly stream: "stdout" | "stderr";
28
+ readonly data: Uint8Array;
29
+ }
30
+
31
+ export interface ContainerHttpReadinessOptions {
32
+ /** Defaults to 30 seconds. All durations accept numbers in milliseconds. */
33
+ readonly timeout?: Duration.Input;
34
+ /** Delay between probes. Defaults to 100 milliseconds. */
35
+ readonly interval?: Duration.Input;
36
+ /** Bounds each connection or HTTP probe. Defaults to 1 second. */
37
+ readonly attemptTimeout?: Duration.Input;
38
+ readonly path?: string;
39
+ /** Accepts any HTTP response unless a status range is supplied. */
40
+ readonly status?: { readonly min: number; readonly max: number };
41
+ }
42
+
43
+ export class ContainerNotConfiguredError extends Data.TaggedError("ContainerNotConfiguredError")<{
44
+ readonly durableObjectId: string;
45
+ }> {
46
+ override get message(): string {
47
+ return `No container is configured for Durable Object "${this.durableObjectId}"`;
48
+ }
49
+ }
50
+
51
+ export class ContainerError extends Data.TaggedError("ContainerError")<{
52
+ readonly operation: string;
53
+ readonly cause: unknown;
54
+ }> {
55
+ override get message(): string {
56
+ return `Durable Object container ${this.operation} failed: ${ErrorMessage.causeMessage(this.cause)}`;
57
+ }
58
+ }
59
+
60
+ export class ContainerReadinessTimeoutError extends Data.TaggedError(
61
+ "ContainerReadinessTimeoutError",
62
+ )<{
63
+ readonly port: number;
64
+ readonly timeout: Duration.Duration;
65
+ /** The last completed probe failure, when available. */
66
+ readonly cause: unknown;
67
+ }> {
68
+ override get message(): string {
69
+ return `Container port ${this.port} was not ready within ${Duration.toMillis(this.timeout)}ms`;
70
+ }
71
+ }
72
+
73
+ /** A native process handle owned by its creating request. Choose one output consumer. */
74
+ export interface ContainerProcess {
75
+ readonly raw: globalThis.ExecProcess;
76
+ readonly pid: number;
77
+ readonly isPty: boolean;
78
+ readonly stdin: globalThis.ExecProcess["stdin"];
79
+ /** Empty when the corresponding native stream is absent. */
80
+ readonly stdout: Stream.Stream<Uint8Array, ContainerError>;
81
+ readonly stderr: Stream.Stream<Uint8Array, ContainerError>;
82
+ /** Drains both channels concurrently. Single-use, without buffering or replay. */
83
+ readonly logs: Stream.Stream<ContainerProcessLog, ContainerError>;
84
+ /** Nonzero exit codes are values, not Effect failures. */
85
+ readonly exitCode: Effect.Effect<number, ContainerError>;
86
+ readonly output: Effect.Effect<ContainerExecOutput, ContainerError>;
87
+ readonly outputText: Effect.Effect<ContainerExecOutputText, ContainerError>;
88
+ /** Signals the main process only; does nothing after its exit has been observed. */
89
+ readonly kill: (signal?: number) => Effect.Effect<void, ContainerError>;
90
+ readonly resize: (cols: number, rows: number) => Effect.Effect<void, ContainerError>;
91
+ }
92
+
93
+ export interface ContainerTcpPort {
94
+ readonly raw: globalThis.Fetcher;
95
+ readonly fetch: (
96
+ input: RequestInfo | URL,
97
+ init?: RequestInit,
98
+ ) => Effect.Effect<Response, ContainerError>;
99
+ /** Forwards an authorized preview request once, using the container's HTTP transport. */
100
+ readonly proxy: (request: Request) => Effect.Effect<Response, ContainerError>;
101
+ /** The caller owns the returned socket and must close it. */
102
+ readonly connect: (
103
+ ...args: Parameters<globalThis.Fetcher["connect"]>
104
+ ) => Effect.Effect<ReturnType<globalThis.Fetcher["connect"]>, ContainerError>;
105
+ }
106
+
107
+ /** Direct container control inside a Durable Object; lifecycle policy belongs to the application. */
108
+ export interface DurableObjectContainerService {
109
+ readonly raw: globalThis.Container;
110
+ readonly running: Effect.Effect<boolean, ContainerError>;
111
+ readonly images: Effect.Effect<Readonly<Record<string, string>>, ContainerError>;
112
+ readonly inspect: Effect.Effect<Option.Option<ContainerInfo>, ContainerError>;
113
+ /** Validates options and initiates startup. It does not wait for readiness. */
114
+ readonly start: (options?: ContainerStartOptions) => Effect.Effect<void, ContainerError>;
115
+ /** Waits for the container to stop; failures retain the native cause, including its exitCode. */
116
+ readonly monitor: Effect.Effect<void, ContainerError>;
117
+ readonly destroy: (
118
+ reason?: Parameters<globalThis.Container["destroy"]>[0],
119
+ ) => Effect.Effect<void, ContainerError>;
120
+ readonly signal: (signal: number) => Effect.Effect<void, ContainerError>;
121
+ /** Effect duration input: numbers are milliseconds; bigints are nanoseconds. */
122
+ readonly setInactivityTimeout: (duration: Duration.Input) => Effect.Effect<void, ContainerError>;
123
+ readonly getTcpPort: (port: number) => Effect.Effect<ContainerTcpPort, ContainerError>;
124
+ /** Sends HTTP GET probes to an already-started container. Never retries a caller's request. */
125
+ readonly waitForHttp: (
126
+ port: number,
127
+ options?: ContainerHttpReadinessOptions,
128
+ ) => Effect.Effect<void, ContainerError | ContainerReadinessTimeoutError>;
129
+ /**
130
+ * Runs an executable with arguments, without a shell or automatic container startup.
131
+ * Interruption cancels acquisition. After acquisition, the caller owns the process.
132
+ * To observe it from later requests, redirect output to files and ignore the native pipes.
133
+ */
134
+ readonly exec: (
135
+ command: ReadonlyArray<string>,
136
+ options?: ContainerExecOptions,
137
+ ) => Effect.Effect<ContainerProcess, ContainerError>;
138
+ /** Sends SIGKILL to a still-running main process when the scope closes. */
139
+ readonly execScoped: (
140
+ command: ReadonlyArray<string>,
141
+ options?: ContainerExecOptions,
142
+ ) => Effect.Effect<ContainerProcess, ContainerError, Scope.Scope>;
143
+ readonly snapshotContainer: (
144
+ options?: ContainerSnapshotOptions,
145
+ ) => Effect.Effect<ContainerSnapshot, ContainerError>;
146
+ readonly interceptOutboundHttp: (
147
+ address: string,
148
+ binding: globalThis.Fetcher,
149
+ ) => Effect.Effect<void, ContainerError>;
150
+ readonly interceptAllOutboundHttp: (
151
+ binding: globalThis.Fetcher,
152
+ ) => Effect.Effect<void, ContainerError>;
153
+ readonly interceptOutboundHttps: (
154
+ address: string,
155
+ binding: globalThis.Fetcher,
156
+ ) => Effect.Effect<void, ContainerError>;
157
+ }
158
+
159
+ export class DurableObjectContainer extends Context.Service<
160
+ DurableObjectContainer,
161
+ DurableObjectContainerService
162
+ >()("effect-cf/DurableObjectContainer") {}
163
+
164
+ const attempt = <A>(operation: string, evaluate: () => A): Effect.Effect<A, ContainerError> =>
165
+ Effect.try({
166
+ try: evaluate,
167
+ catch: (cause) => new ContainerError({ operation, cause }),
168
+ });
169
+
170
+ const attemptPromise = <A>(
171
+ operation: string,
172
+ evaluate: (signal: AbortSignal) => Promise<A>,
173
+ ): Effect.Effect<A, ContainerError> =>
174
+ Effect.tryPromise({
175
+ try: evaluate,
176
+ catch: (cause) => new ContainerError({ operation, cause }),
177
+ });
178
+
179
+ const processStream = (
180
+ stream: globalThis.ExecProcess["stdout"] | undefined,
181
+ operation: string,
182
+ ): Stream.Stream<Uint8Array, ContainerError> =>
183
+ stream === null || stream === undefined
184
+ ? Stream.empty
185
+ : Stream.fromReadableStream({
186
+ evaluate: () => stream,
187
+ onError: (cause) => new ContainerError({ operation, cause }),
188
+ });
189
+
190
+ const fromProcess = (process: globalThis.ExecProcess): ContainerProcess => {
191
+ let exited = false;
192
+ const onExit = () => {
193
+ exited = true;
194
+ };
195
+
196
+ // Signaling an exited native process can raise an uncaught runtime error.
197
+ // Observe completion immediately, even if the caller never awaits exitCode.
198
+ process.exitCode.then(onExit, onExit);
199
+
200
+ const stdout = processStream(process.stdout, "exec.stdout");
201
+ const stderr = processStream(process.stderr, "exec.stderr");
202
+ const output = attemptPromise("exec.output", () => process.output());
203
+
204
+ return {
205
+ raw: process,
206
+ pid: process.pid,
207
+ isPty: process.isPty,
208
+ stdin: process.stdin ?? null,
209
+ stdout,
210
+ stderr,
211
+ logs: Stream.merge(
212
+ Stream.map(stdout, (data): ContainerProcessLog => ({ stream: "stdout", data })),
213
+ Stream.map(stderr, (data): ContainerProcessLog => ({ stream: "stderr", data })),
214
+ ),
215
+ exitCode: attemptPromise("exec.exitCode", () => process.exitCode),
216
+ output,
217
+ outputText: Effect.map(output, (result) => ({
218
+ stdout: new TextDecoder().decode(result.stdout),
219
+ stderr: new TextDecoder().decode(result.stderr),
220
+ exitCode: result.exitCode,
221
+ })),
222
+ kill: (signal) =>
223
+ attemptPromise("exec.kill", async () => {
224
+ // Let an already-settled exitCode notify us before attempting a signal.
225
+ await Promise.resolve();
226
+ if (!exited) process.kill(signal);
227
+ }),
228
+ resize: (cols, rows) => attempt("exec.resize", () => process.resize(cols, rows)),
229
+ };
230
+ };
231
+
232
+ const fromTcpPort = (port: globalThis.Fetcher): ContainerTcpPort => {
233
+ const fetch: ContainerTcpPort["fetch"] = (input, init) =>
234
+ attemptPromise("getTcpPort.fetch", (signal) => {
235
+ const callerSignal =
236
+ init?.signal !== undefined
237
+ ? init.signal
238
+ : input instanceof Request
239
+ ? input.signal
240
+ : undefined;
241
+
242
+ return port.fetch(input, {
243
+ ...init,
244
+ signal:
245
+ callerSignal === null || callerSignal === undefined
246
+ ? signal
247
+ : AbortSignal.any([callerSignal, signal]),
248
+ });
249
+ });
250
+
251
+ return {
252
+ raw: port,
253
+ fetch,
254
+ proxy: (request) =>
255
+ attempt("getTcpPort.proxy", () => {
256
+ const url = new URL(request.url);
257
+
258
+ url.protocol = "http:";
259
+
260
+ return new Request(url, request);
261
+ }).pipe(Effect.flatMap((forwarded) => fetch(forwarded))),
262
+ connect: (...args) => attempt("getTcpPort.connect", () => port.connect(...args)),
263
+ };
264
+ };
265
+
266
+ /** Wraps `ctx.container` without starting it or taking ownership of its lifecycle. */
267
+ export const fromContainer = (container: globalThis.Container): DurableObjectContainerService => {
268
+ const exec = (command: ReadonlyArray<string>, options?: ContainerExecOptions) =>
269
+ attemptPromise("exec", (signal) => container.exec([...command], { ...options, signal })).pipe(
270
+ Effect.map(fromProcess),
271
+ );
272
+ const waitForHttp = Effect.fnUntraced(function* (
273
+ portNumber: number,
274
+ options: ContainerHttpReadinessOptions = {},
275
+ ) {
276
+ const timeout = yield* attempt("waitForHttp", () =>
277
+ Duration.fromInputUnsafe(options.timeout ?? "30 seconds"),
278
+ );
279
+ let lastFailure: unknown;
280
+ const probe = attemptPromise("waitForHttp", async (signal) => {
281
+ // Reacquire after a failed probe so workerd can discard a failed cached port capability.
282
+ const response = await container
283
+ .getTcpPort(portNumber)
284
+ .fetch(new URL(options.path ?? "/", "http://container"), { signal });
285
+
286
+ await response.body?.cancel();
287
+ if (
288
+ options.status !== undefined &&
289
+ (response.status < options.status.min || response.status > options.status.max)
290
+ ) {
291
+ throw new Error(`HTTP readiness probe returned status ${response.status}`);
292
+ }
293
+ });
294
+ const poll = Effect.gen(function* () {
295
+ while (true) {
296
+ if (!(yield* attempt("running", () => container.running))) {
297
+ return yield* new ContainerError({
298
+ operation: "waitForHttp",
299
+ cause: new Error("Container stopped before the port became ready"),
300
+ });
301
+ }
302
+ const ready = yield* probe.pipe(
303
+ Effect.timeout(options.attemptTimeout ?? "1 second"),
304
+ Effect.match({
305
+ onSuccess: () => true,
306
+ onFailure: (error) => {
307
+ lastFailure = error;
308
+
309
+ return false;
310
+ },
311
+ }),
312
+ );
313
+
314
+ if (ready) return;
315
+ yield* Effect.sleep(options.interval ?? "100 millis");
316
+ }
317
+ });
318
+
319
+ return yield* poll.pipe(
320
+ Effect.timeoutOrElse({
321
+ duration: timeout,
322
+ orElse: () =>
323
+ Effect.fail(
324
+ new ContainerReadinessTimeoutError({ port: portNumber, timeout, cause: lastFailure }),
325
+ ),
326
+ }),
327
+ );
328
+ });
329
+
330
+ return {
331
+ raw: container,
332
+ running: attempt("running", () => container.running),
333
+ images: attempt("images", () => container.images),
334
+ inspect: attemptPromise("inspect", () => container.inspect()).pipe(
335
+ Effect.map(Option.fromNullishOr),
336
+ ),
337
+ start: (options) => attempt("start", () => container.start(options)),
338
+ monitor: attemptPromise("monitor", () => container.monitor()),
339
+ destroy: (reason) => attemptPromise("destroy", () => container.destroy(reason)),
340
+ signal: (signal) => attempt("signal", () => container.signal(signal)),
341
+ setInactivityTimeout: (duration) =>
342
+ attemptPromise("setInactivityTimeout", () =>
343
+ container.setInactivityTimeout(Duration.toMillis(duration)),
344
+ ),
345
+ getTcpPort: (port) => attempt("getTcpPort", () => fromTcpPort(container.getTcpPort(port))),
346
+ waitForHttp,
347
+ exec,
348
+ execScoped: (command, options) =>
349
+ Effect.acquireRelease(
350
+ exec(command, options),
351
+ (process) => process.kill(9).pipe(Effect.orDie),
352
+ {
353
+ interruptible: true,
354
+ },
355
+ ),
356
+ snapshotContainer: (options = {}) =>
357
+ attemptPromise("snapshotContainer", () => container.snapshotContainer(options)),
358
+ interceptOutboundHttp: (address, binding) =>
359
+ attemptPromise("interceptOutboundHttp", () =>
360
+ container.interceptOutboundHttp(address, binding),
361
+ ),
362
+ interceptAllOutboundHttp: (binding) =>
363
+ attemptPromise("interceptAllOutboundHttp", () => container.interceptAllOutboundHttp(binding)),
364
+ interceptOutboundHttps: (address, binding) =>
365
+ attemptPromise("interceptOutboundHttps", () =>
366
+ container.interceptOutboundHttps(address, binding),
367
+ ),
368
+ };
369
+ };
370
+
371
+ /** Supplies the container attached to the current Durable Object, without owning it. */
372
+ export const layer: Layer.Layer<
373
+ DurableObjectContainer,
374
+ ContainerNotConfiguredError,
375
+ DurableObjectState
376
+ > = Layer.effect(
377
+ DurableObjectContainer,
378
+ Effect.gen(function* () {
379
+ const state = yield* DurableObjectState;
380
+ const container = state.raw.container;
381
+
382
+ if (container === undefined) {
383
+ return yield* new ContainerNotConfiguredError({ durableObjectId: state.id.toString() });
384
+ }
385
+
386
+ return fromContainer(container);
387
+ }),
388
+ );
@@ -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")<{
package/src/Sandbox.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Adapter for the legacy `@cloudflare/sandbox` 0.13 namespace/client API.
3
+ * SDK 1.x utilities can instead use `DurableObjectContainer`'s native `raw` handle.
4
+ */
1
5
  import type {
2
6
  BackupOptions,
3
7
  CheckChangesOptions,
package/src/index.ts CHANGED
@@ -6,10 +6,12 @@ export * as BrowserRendering from "./BrowserRendering";
6
6
  export * as Cache from "./Cache";
7
7
  export * as CloudflareOtlp from "./CloudflareOtlp";
8
8
  export * as CloudflareTracer from "./CloudflareTracer";
9
+ export * as ContainerFiles from "./ContainerFiles";
9
10
  export * as ContainerNamespace from "./ContainerNamespace";
10
11
  export * as D1 from "./D1";
11
12
  export * as DurableObject from "./DurableObject";
12
13
  export * as DurableObjectAlarm from "./DurableObjectAlarm";
14
+ export * as DurableObjectContainer from "./DurableObjectContainer";
13
15
  export * as DurableObjectDefinition from "./DurableObjectDefinition";
14
16
  export * as DurableObjectNamespace from "./DurableObjectNamespace";
15
17
  export * as DurableObjectRpcWebSocket from "./DurableObjectRpcWebSocket";