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,179 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { causeMessage } from "./internal/ErrorMessage.mjs";
3
+ import { DurableObjectState } from "./DurableObjectState.mjs";
4
+ import * as Context from "effect/Context";
5
+ import * as Data from "effect/Data";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Layer from "effect/Layer";
8
+ import * as Option from "effect/Option";
9
+ import * as Stream from "effect/Stream";
10
+ import * as Duration from "effect/Duration";
11
+ //#region src/DurableObjectContainer.ts
12
+ var DurableObjectContainer_exports = /* @__PURE__ */ __exportAll({
13
+ ContainerError: () => ContainerError,
14
+ ContainerNotConfiguredError: () => ContainerNotConfiguredError,
15
+ ContainerReadinessTimeoutError: () => ContainerReadinessTimeoutError,
16
+ DurableObjectContainer: () => DurableObjectContainer,
17
+ fromContainer: () => fromContainer,
18
+ layer: () => layer
19
+ });
20
+ var ContainerNotConfiguredError = class extends Data.TaggedError("ContainerNotConfiguredError") {
21
+ get message() {
22
+ return `No container is configured for Durable Object "${this.durableObjectId}"`;
23
+ }
24
+ };
25
+ var ContainerError = class extends Data.TaggedError("ContainerError") {
26
+ get message() {
27
+ return `Durable Object container ${this.operation} failed: ${causeMessage(this.cause)}`;
28
+ }
29
+ };
30
+ var ContainerReadinessTimeoutError = class extends Data.TaggedError("ContainerReadinessTimeoutError") {
31
+ get message() {
32
+ return `Container port ${this.port} was not ready within ${Duration.toMillis(this.timeout)}ms`;
33
+ }
34
+ };
35
+ var DurableObjectContainer = class extends Context.Service()("effect-cf/DurableObjectContainer") {};
36
+ const attempt = (operation, evaluate) => Effect.try({
37
+ try: evaluate,
38
+ catch: (cause) => new ContainerError({
39
+ operation,
40
+ cause
41
+ })
42
+ });
43
+ const attemptPromise = (operation, evaluate) => Effect.tryPromise({
44
+ try: evaluate,
45
+ catch: (cause) => new ContainerError({
46
+ operation,
47
+ cause
48
+ })
49
+ });
50
+ const processStream = (stream, operation) => stream === null || stream === void 0 ? Stream.empty : Stream.fromReadableStream({
51
+ evaluate: () => stream,
52
+ onError: (cause) => new ContainerError({
53
+ operation,
54
+ cause
55
+ })
56
+ });
57
+ const fromProcess = (process) => {
58
+ let exited = false;
59
+ const onExit = () => {
60
+ exited = true;
61
+ };
62
+ process.exitCode.then(onExit, onExit);
63
+ const stdout = processStream(process.stdout, "exec.stdout");
64
+ const stderr = processStream(process.stderr, "exec.stderr");
65
+ const output = attemptPromise("exec.output", () => process.output());
66
+ return {
67
+ raw: process,
68
+ pid: process.pid,
69
+ isPty: process.isPty,
70
+ stdin: process.stdin ?? null,
71
+ stdout,
72
+ stderr,
73
+ logs: Stream.merge(Stream.map(stdout, (data) => ({
74
+ stream: "stdout",
75
+ data
76
+ })), Stream.map(stderr, (data) => ({
77
+ stream: "stderr",
78
+ data
79
+ }))),
80
+ exitCode: attemptPromise("exec.exitCode", () => process.exitCode),
81
+ output,
82
+ outputText: Effect.map(output, (result) => ({
83
+ stdout: new TextDecoder().decode(result.stdout),
84
+ stderr: new TextDecoder().decode(result.stderr),
85
+ exitCode: result.exitCode
86
+ })),
87
+ kill: (signal) => attemptPromise("exec.kill", async () => {
88
+ await Promise.resolve();
89
+ if (!exited) process.kill(signal);
90
+ }),
91
+ resize: (cols, rows) => attempt("exec.resize", () => process.resize(cols, rows))
92
+ };
93
+ };
94
+ const fromTcpPort = (port) => {
95
+ const fetch = (input, init) => attemptPromise("getTcpPort.fetch", (signal) => {
96
+ const callerSignal = init?.signal !== void 0 ? init.signal : input instanceof Request ? input.signal : void 0;
97
+ return port.fetch(input, {
98
+ ...init,
99
+ signal: callerSignal === null || callerSignal === void 0 ? signal : AbortSignal.any([callerSignal, signal])
100
+ });
101
+ });
102
+ return {
103
+ raw: port,
104
+ fetch,
105
+ proxy: (request) => attempt("getTcpPort.proxy", () => {
106
+ const url = new URL(request.url);
107
+ url.protocol = "http:";
108
+ return new Request(url, request);
109
+ }).pipe(Effect.flatMap((forwarded) => fetch(forwarded))),
110
+ connect: (...args) => attempt("getTcpPort.connect", () => port.connect(...args))
111
+ };
112
+ };
113
+ /** Wraps `ctx.container` without starting it or taking ownership of its lifecycle. */
114
+ const fromContainer = (container) => {
115
+ const exec = (command, options) => attemptPromise("exec", (signal) => container.exec([...command], {
116
+ ...options,
117
+ signal
118
+ })).pipe(Effect.map(fromProcess));
119
+ const waitForHttp = Effect.fnUntraced(function* (portNumber, options = {}) {
120
+ const timeout = yield* attempt("waitForHttp", () => Duration.fromInputUnsafe(options.timeout ?? "30 seconds"));
121
+ let lastFailure;
122
+ const probe = attemptPromise("waitForHttp", async (signal) => {
123
+ const response = await container.getTcpPort(portNumber).fetch(new URL(options.path ?? "/", "http://container"), { signal });
124
+ await response.body?.cancel();
125
+ if (options.status !== void 0 && (response.status < options.status.min || response.status > options.status.max)) throw new Error(`HTTP readiness probe returned status ${response.status}`);
126
+ });
127
+ return yield* Effect.gen(function* () {
128
+ while (true) {
129
+ if (!(yield* attempt("running", () => container.running))) return yield* new ContainerError({
130
+ operation: "waitForHttp",
131
+ cause: /* @__PURE__ */ new Error("Container stopped before the port became ready")
132
+ });
133
+ if (yield* probe.pipe(Effect.timeout(options.attemptTimeout ?? "1 second"), Effect.match({
134
+ onSuccess: () => true,
135
+ onFailure: (error) => {
136
+ lastFailure = error;
137
+ return false;
138
+ }
139
+ }))) return;
140
+ yield* Effect.sleep(options.interval ?? "100 millis");
141
+ }
142
+ }).pipe(Effect.timeoutOrElse({
143
+ duration: timeout,
144
+ orElse: () => Effect.fail(new ContainerReadinessTimeoutError({
145
+ port: portNumber,
146
+ timeout,
147
+ cause: lastFailure
148
+ }))
149
+ }));
150
+ });
151
+ return {
152
+ raw: container,
153
+ running: attempt("running", () => container.running),
154
+ images: attempt("images", () => container.images),
155
+ inspect: attemptPromise("inspect", () => container.inspect()).pipe(Effect.map(Option.fromNullishOr)),
156
+ start: (options) => attempt("start", () => container.start(options)),
157
+ monitor: attemptPromise("monitor", () => container.monitor()),
158
+ destroy: (reason) => attemptPromise("destroy", () => container.destroy(reason)),
159
+ signal: (signal) => attempt("signal", () => container.signal(signal)),
160
+ setInactivityTimeout: (duration) => attemptPromise("setInactivityTimeout", () => container.setInactivityTimeout(Duration.toMillis(duration))),
161
+ getTcpPort: (port) => attempt("getTcpPort", () => fromTcpPort(container.getTcpPort(port))),
162
+ waitForHttp,
163
+ exec,
164
+ execScoped: (command, options) => Effect.acquireRelease(exec(command, options), (process) => process.kill(9).pipe(Effect.orDie), { interruptible: true }),
165
+ snapshotContainer: (options = {}) => attemptPromise("snapshotContainer", () => container.snapshotContainer(options)),
166
+ interceptOutboundHttp: (address, binding) => attemptPromise("interceptOutboundHttp", () => container.interceptOutboundHttp(address, binding)),
167
+ interceptAllOutboundHttp: (binding) => attemptPromise("interceptAllOutboundHttp", () => container.interceptAllOutboundHttp(binding)),
168
+ interceptOutboundHttps: (address, binding) => attemptPromise("interceptOutboundHttps", () => container.interceptOutboundHttps(address, binding))
169
+ };
170
+ };
171
+ /** Supplies the container attached to the current Durable Object, without owning it. */
172
+ const layer = Layer.effect(DurableObjectContainer, Effect.gen(function* () {
173
+ const state = yield* DurableObjectState;
174
+ const container = state.raw.container;
175
+ if (container === void 0) return yield* new ContainerNotConfiguredError({ durableObjectId: state.id.toString() });
176
+ return fromContainer(container);
177
+ }));
178
+ //#endregion
179
+ export { ContainerError, ContainerNotConfiguredError, ContainerReadinessTimeoutError, DurableObjectContainer, DurableObjectContainer_exports, fromContainer, layer };
@@ -64,8 +64,8 @@ interface LayerOptions {
64
64
  readonly attachmentKey?: string | undefined;
65
65
  /**
66
66
  * Heartbeat behavior. `"auto-response"` (the default) installs Cloudflare's
67
- * hibernation-safe text Ping/Pong response while an RPC socket exists and no
68
- * non-resumable RPC operation is active, then removes it while one is pending.
67
+ * hibernation-safe text Ping/Pong response for the lifetime of RPC sockets,
68
+ * including pending operations. Keepalives never dispatch application code.
69
69
  * `"passthrough"` leaves the Durable Object's existing auto-response
70
70
  * configuration unchanged and makes the application responsible for waking a
71
71
  * lost pending operation.
@@ -70,7 +70,6 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
70
70
  const declarationsByTag = /* @__PURE__ */ new Map();
71
71
  const replayRequests = [];
72
72
  let nextClientId = 0;
73
- let activeNonResumableRequestCount = 0;
74
73
  let writeRequest;
75
74
  for (const declaration of declarations) {
76
75
  if (declaration.id.length === 0 || declaration.rpcTag.length === 0) return yield* Effect.die(/* @__PURE__ */ new Error("Resumable RPC stream declaration ids and tags must be non-empty"));
@@ -137,7 +136,7 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
137
136
  return connection;
138
137
  };
139
138
  const heartbeat = yield* makeHeartbeatController(durableObjectState, serialization, heartbeatPolicy);
140
- const syncHeartbeat = () => heartbeat.setEnabled(connectionsBySocket.size > 0 && activeNonResumableRequestCount === 0);
139
+ const syncHeartbeat = () => heartbeat.setEnabled(connectionsBySocket.size > 0);
141
140
  const persistConnection = Effect.fn("DurableObjectRpcWebSocket.persistConnection")(function* (connection) {
142
141
  return yield* Effect.try({
143
142
  try: () => {
@@ -171,7 +170,6 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
171
170
  if (connection === void 0) return;
172
171
  connectionsBySocket.delete(socket.raw);
173
172
  connectionsById.delete(connection.id);
174
- activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
175
173
  connection.requestIds.clear();
176
174
  connection.nonResumableRequestIds.clear();
177
175
  connection.subscriptionsByRequestId.clear();
@@ -210,7 +208,6 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
210
208
  } else {
211
209
  connection.requestIds.add(request.id);
212
210
  connection.nonResumableRequestIds.add(request.id);
213
- activeNonResumableRequestCount++;
214
211
  }
215
212
  if (!(yield* persistOrReset(connection))) return false;
216
213
  yield* syncHeartbeat();
@@ -218,7 +215,7 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
218
215
  });
219
216
  const completeRequest = Effect.fn("DurableObjectRpcWebSocket.completeRequest")(function* (connection, requestId, preserveAckFilter = false) {
220
217
  if (connection.requestIds.delete(requestId)) {
221
- if (connection.nonResumableRequestIds.delete(requestId)) activeNonResumableRequestCount--;
218
+ connection.nonResumableRequestIds.delete(requestId);
222
219
  const subscription = removeSubscription(connection, requestId);
223
220
  connection.currentPendingRequestIds.delete(requestId);
224
221
  if (preserveAckFilter && subscription !== void 0) connection.suppressedAckRequestIds.add(requestId);
@@ -229,7 +226,6 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
229
226
  });
230
227
  const completeAllRequests = Effect.fn("DurableObjectRpcWebSocket.completeAllRequests")(function* (connection) {
231
228
  const hadRequests = connection.requestIds.size > 0;
232
- if (connection.requestIds.size > 0) activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
233
229
  connection.requestIds.clear();
234
230
  connection.nonResumableRequestIds.clear();
235
231
  connection.subscriptionsByRequestId.clear();
@@ -2,7 +2,6 @@ import * as Effect from "effect/Effect";
2
2
  import * as S from "effect/Schema";
3
3
  import * as Option from "effect/Option";
4
4
  import * as Cause from "effect/Cause";
5
- import * as Duration from "effect/Duration";
6
5
  declare namespace DurableObjectStorage_d_exports {
7
6
  export { DurableObjectStorage, DurableObjectTransaction, SchemaBackedSyncKvStorage, SqlCursor, SqlStorage, SqlStorageValue, StorageOperationError, SyncKvDefinition, SyncKvStorage, fromDurableObjectStorage };
8
7
  }
@@ -69,23 +68,17 @@ interface DurableObjectTransaction {
69
68
  delete(keys: Array<string>, options?: globalThis.DurableObjectPutOptions): StorageEffect<number>;
70
69
  rollback(): StorageEffect<void>;
71
70
  getAlarm(options?: globalThis.DurableObjectGetAlarmOptions): StorageEffect<number | null>;
72
- setAlarm(scheduledTime: number | Date, options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
73
- setAlarmAfter(delay: Duration.Input, options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
74
- deleteAlarm(options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
75
71
  }
76
72
  interface DurableObjectStorage {
77
73
  get<T = unknown>(key: string, options?: globalThis.DurableObjectGetOptions): StorageEffect<T | undefined>;
78
74
  put<T>(key: string, value: T, options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
79
75
  delete(key: string, options?: globalThis.DurableObjectPutOptions): StorageEffect<boolean>;
80
76
  /**
81
- * Deletes all stored data. On compatibility dates before 2026-02-24, Cloudflare
82
- * documents that active alarms must be deleted separately with `deleteAlarm()`.
77
+ * Deletes all stored data, including logical schedules. Use the alarm scheduler
78
+ * to cancel individual alarms without deleting application data.
83
79
  */
84
80
  deleteAll(options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
85
81
  getAlarm(options?: globalThis.DurableObjectGetAlarmOptions): StorageEffect<number | null>;
86
- setAlarm(scheduledTime: number | Date, options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
87
- setAlarmAfter(delay: Duration.Input, options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
88
- deleteAlarm(options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
89
82
  /**
90
83
  * SQLite-backed Durable Object storage only.
91
84
  *
@@ -9,8 +9,6 @@ import * as Option from "effect/Option";
9
9
  import * as Result from "effect/Result";
10
10
  import * as Cause from "effect/Cause";
11
11
  import * as Exit from "effect/Exit";
12
- import * as Clock from "effect/Clock";
13
- import * as Duration from "effect/Duration";
14
12
  import * as Fiber from "effect/Fiber";
15
13
  //#region src/DurableObjectStorage.ts
16
14
  var DurableObjectStorage_exports = /* @__PURE__ */ __exportAll({
@@ -34,11 +32,6 @@ const tryStoragePromise = (operation, evaluate) => Effect.tryPromise({
34
32
  try: evaluate,
35
33
  catch: (cause) => storageError(operation, cause)
36
34
  });
37
- const setAlarmAfter = Effect.fn("DurableObjectStorage.setAlarmAfter")(function* (operation, setAlarm, delay, options) {
38
- const now = yield* Clock.currentTimeMillis;
39
- const delayMillis = Math.ceil(Duration.toMillis(delay));
40
- yield* tryStoragePromise(operation, () => setAlarm(now + delayMillis, options));
41
- });
42
35
  const fromSqlCursor = (cursor) => ({
43
36
  next: () => tryStorageSync("sql.next", () => cursor.next()),
44
37
  toArray: () => tryStorageSync("sql.toArray", () => cursor.toArray()),
@@ -98,10 +91,7 @@ const fromDurableObjectTransaction = (txn) => ({
98
91
  put: (keyOrEntries, valueOrOptions, maybeOptions) => tryStoragePromise("transaction.put", () => Predicate.isString(keyOrEntries) ? txn.put(keyOrEntries, valueOrOptions, maybeOptions) : txn.put(keyOrEntries, valueOrOptions)),
99
92
  delete: (keyOrKeys, options) => Array.isArray(keyOrKeys) ? tryStoragePromise("transaction.delete", () => txn.delete(keyOrKeys, options)) : tryStoragePromise("transaction.delete", () => txn.delete(keyOrKeys, options)),
100
93
  rollback: () => tryStorageSync("transaction.rollback", () => txn.rollback()),
101
- getAlarm: (options) => tryStoragePromise("transaction.getAlarm", () => txn.getAlarm(options)),
102
- setAlarm: (scheduledTime, options) => tryStoragePromise("transaction.setAlarm", () => txn.setAlarm(scheduledTime, options)),
103
- setAlarmAfter: (delay, options) => setAlarmAfter("transaction.setAlarm", (scheduledTime, options) => txn.setAlarm(scheduledTime, options), delay, options),
104
- deleteAlarm: (options) => tryStoragePromise("transaction.deleteAlarm", () => txn.deleteAlarm(options))
94
+ getAlarm: (options) => tryStoragePromise("transaction.getAlarm", () => txn.getAlarm(options))
105
95
  });
106
96
  const fromDurableObjectStorage = (storage) => ({
107
97
  get: (key, options) => tryStoragePromise("get", () => storage.get(key, options)),
@@ -109,9 +99,6 @@ const fromDurableObjectStorage = (storage) => ({
109
99
  delete: (key, options) => tryStoragePromise("delete", () => storage.delete(key, options)),
110
100
  deleteAll: (options) => tryStoragePromise("deleteAll", () => storage.deleteAll(options)),
111
101
  getAlarm: (options) => tryStoragePromise("getAlarm", () => storage.getAlarm(options)),
112
- setAlarm: (scheduledTime, options) => tryStoragePromise("setAlarm", () => storage.setAlarm(scheduledTime, options)),
113
- setAlarmAfter: (delay, options) => setAlarmAfter("setAlarm", (scheduledTime, options) => storage.setAlarm(scheduledTime, options), delay, options),
114
- deleteAlarm: (options) => tryStoragePromise("deleteAlarm", () => storage.deleteAlarm(options)),
115
102
  transactionSync: (closure) => Effect.context().pipe(Effect.flatMap((context) => Effect.suspend(() => {
116
103
  let aborted;
117
104
  let escapedFiber;
@@ -3,8 +3,16 @@ import * as Effect from "effect/Effect";
3
3
  import * as S from "effect/Schema";
4
4
  import * as Option from "effect/Option";
5
5
  declare namespace DurableObjectWebSocket_d_exports {
6
- export { AcceptUpgradeOptions, AcceptedUpgrade, AttachmentInvalidPolicy, AttachmentRehydrateOptions, DurableWebSocket, DurableWebSocketAttachment, DurableWebSocketAttachmentError, DurableWebSocketCloseError, DurableWebSocketHandlers, DurableWebSocketSendData, DurableWebSocketSendError, RehydratedDurableWebSocket, acceptUpgrade, attachment, fromWebSocket, handlers };
6
+ export { AcceptUpgradeOptions, AcceptedUpgrade, AttachmentInvalidPolicy, AttachmentRehydrateOptions, DEFAULT_KEEPALIVE_REQUEST, DEFAULT_KEEPALIVE_RESPONSE, DurableWebSocket, DurableWebSocketAttachment, DurableWebSocketAttachmentError, DurableWebSocketCloseError, DurableWebSocketHandlers, DurableWebSocketSendData, DurableWebSocketSendError, RehydratedDurableWebSocket, acceptUpgrade, attachment, fromWebSocket, handlers, installKeepalive };
7
7
  }
8
+ declare const DEFAULT_KEEPALIVE_REQUEST = "effect-cf:ping";
9
+ declare const DEFAULT_KEEPALIVE_RESPONSE = "effect-cf:pong";
10
+ /**
11
+ * Registers exact text keepalives with Cloudflare so they never invoke the object.
12
+ * The pair applies to all hibernating sockets on this object; install in its constructor.
13
+ * Clients send the request text and consume the response outside their application protocol.
14
+ */
15
+ declare const installKeepalive: (request?: any, response?: any) => Effect.Effect<undefined, never, DurableObjectState>;
8
16
  type DurableWebSocketSendData = string | ArrayBuffer | ArrayBufferView;
9
17
  declare const DurableWebSocketSendError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
10
18
  readonly _tag: "DurableWebSocketSendError";
@@ -80,4 +88,4 @@ declare const handlers: <R = never, E = unknown>(options: DurableWebSocketHandle
80
88
  webSocketError: ((socket: DurableWebSocket, cause: unknown) => Effect.Effect<void, E, R>) | undefined;
81
89
  };
82
90
  //#endregion
83
- export { AcceptUpgradeOptions, AcceptedUpgrade, AttachmentInvalidPolicy, AttachmentRehydrateOptions, DurableObjectWebSocket_d_exports, DurableWebSocket, DurableWebSocketAttachment, DurableWebSocketAttachmentError, DurableWebSocketCloseError, DurableWebSocketHandlers, DurableWebSocketSendData, DurableWebSocketSendError, RehydratedDurableWebSocket, acceptUpgrade, attachment, fromWebSocket, handlers };
91
+ export { AcceptUpgradeOptions, AcceptedUpgrade, AttachmentInvalidPolicy, AttachmentRehydrateOptions, DEFAULT_KEEPALIVE_REQUEST, DEFAULT_KEEPALIVE_RESPONSE, DurableObjectWebSocket_d_exports, DurableWebSocket, DurableWebSocketAttachment, DurableWebSocketAttachmentError, DurableWebSocketCloseError, DurableWebSocketHandlers, DurableWebSocketSendData, DurableWebSocketSendError, RehydratedDurableWebSocket, acceptUpgrade, attachment, fromWebSocket, handlers, installKeepalive };
@@ -8,13 +8,29 @@ import * as Option from "effect/Option";
8
8
  import * as Result from "effect/Result";
9
9
  //#region src/DurableObjectWebSocket.ts
10
10
  var DurableObjectWebSocket_exports = /* @__PURE__ */ __exportAll({
11
+ DEFAULT_KEEPALIVE_REQUEST: () => DEFAULT_KEEPALIVE_REQUEST,
12
+ DEFAULT_KEEPALIVE_RESPONSE: () => DEFAULT_KEEPALIVE_RESPONSE,
11
13
  DurableWebSocketAttachmentError: () => DurableWebSocketAttachmentError,
12
14
  DurableWebSocketCloseError: () => DurableWebSocketCloseError,
13
15
  DurableWebSocketSendError: () => DurableWebSocketSendError,
14
16
  acceptUpgrade: () => acceptUpgrade,
15
17
  attachment: () => attachment,
16
18
  fromWebSocket: () => fromWebSocket,
17
- handlers: () => handlers
19
+ handlers: () => handlers,
20
+ installKeepalive: () => installKeepalive
21
+ });
22
+ const DEFAULT_KEEPALIVE_REQUEST = "effect-cf:ping";
23
+ const DEFAULT_KEEPALIVE_RESPONSE = "effect-cf:pong";
24
+ /**
25
+ * Registers exact text keepalives with Cloudflare so they never invoke the object.
26
+ * The pair applies to all hibernating sockets on this object; install in its constructor.
27
+ * Clients send the request text and consume the response outside their application protocol.
28
+ */
29
+ const installKeepalive = Effect.fn("DurableObjectWebSocket.installKeepalive")(function* (request = DEFAULT_KEEPALIVE_REQUEST, response = DEFAULT_KEEPALIVE_RESPONSE) {
30
+ const state = yield* DurableObjectState;
31
+ const configured = yield* state.getWebSocketAutoResponse;
32
+ if (configured !== null && (configured.request !== request || configured.response !== response)) return yield* Effect.die(/* @__PURE__ */ new Error("Durable Object already has a different keepalive protocol"));
33
+ yield* state.setWebSocketAutoResponse(new WebSocketRequestResponsePair(request, response));
18
34
  });
19
35
  var DurableWebSocketSendError = class extends Data.TaggedError("DurableWebSocketSendError") {
20
36
  get message() {
@@ -117,4 +133,4 @@ const handlers = (options) => ({
117
133
  webSocketError: options.error
118
134
  });
119
135
  //#endregion
120
- export { DurableObjectWebSocket_exports, DurableWebSocketAttachmentError, DurableWebSocketCloseError, DurableWebSocketSendError, acceptUpgrade, attachment, fromWebSocket, handlers };
136
+ export { DEFAULT_KEEPALIVE_REQUEST, DEFAULT_KEEPALIVE_RESPONSE, DurableObjectWebSocket_exports, DurableWebSocketAttachmentError, DurableWebSocketCloseError, DurableWebSocketSendError, acceptUpgrade, attachment, fromWebSocket, handlers, installKeepalive };
@@ -5,9 +5,9 @@ import * as Schema from "effect/Schema";
5
5
  import * as Option from "effect/Option";
6
6
  import * as Cause from "effect/Cause";
7
7
  import * as Exit from "effect/Exit";
8
- import * as Clock from "effect/Clock";
9
8
  import * as Tracer from "effect/Tracer";
10
9
  import * as References from "effect/References";
10
+ import * as Clock from "effect/Clock";
11
11
  //#region src/RpcTracing.ts
12
12
  var RpcTracing_exports = /* @__PURE__ */ __exportAll({
13
13
  RpcTraceContext: () => RpcTraceContext,
package/dist/index.d.mts CHANGED
@@ -24,6 +24,8 @@ import { BrowserRendering_d_exports } from "./BrowserRendering.mjs";
24
24
  import { Cache_d_exports } from "./Cache.mjs";
25
25
  import { CloudflareOtlp_d_exports } from "./CloudflareOtlp.mjs";
26
26
  import { CloudflareTracer_d_exports } from "./CloudflareTracer.mjs";
27
+ import { DurableObjectContainer_d_exports } from "./DurableObjectContainer.mjs";
28
+ import { ContainerFiles_d_exports } from "./ContainerFiles.mjs";
27
29
  import { D1_d_exports } from "./D1.mjs";
28
30
  import { DurableObjectAlarm_d_exports } from "./DurableObjectAlarm.mjs";
29
31
  import { DurableObjectDefinition_d_exports } from "./DurableObjectDefinition.mjs";
@@ -40,4 +42,4 @@ import { Vectorize_d_exports } from "./Vectorize.mjs";
40
42
  import { WebTransport_d_exports } from "./WebTransport.mjs";
41
43
  import { WorkflowDefinition_d_exports } from "./WorkflowDefinition.mjs";
42
44
  import { Workflow_d_exports } from "./Workflow.mjs";
43
- export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTargets_d_exports as RpcTargets, RpcTracing_d_exports as RpcTracing, RpcWebSocketClient_d_exports as RpcWebSocketClient, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
45
+ export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerFiles_d_exports as ContainerFiles, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectContainer_d_exports as DurableObjectContainer, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTargets_d_exports as RpcTargets, RpcTracing_d_exports as RpcTracing, RpcWebSocketClient_d_exports as RpcWebSocketClient, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
package/dist/index.mjs CHANGED
@@ -11,6 +11,8 @@ import { DurableObjectWebSocket_exports } from "./DurableObjectWebSocket.mjs";
11
11
  import { DurableObjectState_exports } from "./DurableObjectState.mjs";
12
12
  import { CloudflareOtlp_exports } from "./CloudflareOtlp.mjs";
13
13
  import { CloudflareTracer_exports } from "./CloudflareTracer.mjs";
14
+ import { DurableObjectContainer_exports } from "./DurableObjectContainer.mjs";
15
+ import { ContainerFiles_exports } from "./ContainerFiles.mjs";
14
16
  import { RpcTargets_exports } from "./RpcTargets.mjs";
15
17
  import { ContainerNamespace_exports } from "./ContainerNamespace.mjs";
16
18
  import { D1_exports } from "./D1.mjs";
@@ -40,4 +42,4 @@ import { WebTransport_exports } from "./WebTransport.mjs";
40
42
  import { WorkflowBinding_exports } from "./WorkflowBinding.mjs";
41
43
  import { WorkflowDefinition_exports } from "./WorkflowDefinition.mjs";
42
44
  import { Workflow_exports } from "./Workflow.mjs";
43
- export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTargets_exports as RpcTargets, RpcTracing_exports as RpcTracing, RpcWebSocketClient_exports as RpcWebSocketClient, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
45
+ export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerFiles_exports as ContainerFiles, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectContainer_exports as DurableObjectContainer, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTargets_exports as RpcTargets, RpcTracing_exports as RpcTracing, RpcWebSocketClient_exports as RpcWebSocketClient, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -78,6 +78,7 @@
78
78
  "@cloudflare/computer": "0.2.0",
79
79
  "@cloudflare/containers": "catalog:",
80
80
  "@cloudflare/sandbox": "0.13.0-next.738.2",
81
+ "@cloudflare/sandbox-v1": "npm:@cloudflare/sandbox@1.0.0",
81
82
  "@cloudflare/vitest-plugin": "catalog:",
82
83
  "@cloudflare/workers-types": "catalog:",
83
84
  "@effect/platform-node": "catalog:",
@@ -96,9 +97,9 @@
96
97
  },
97
98
  "peerDependencies": {
98
99
  "@cloudflare/computer": "^0.2.0",
99
- "@cloudflare/sandbox": "^0.13.0-next.738.2",
100
+ "@cloudflare/sandbox": "^0.13.0-next.738.2 || ^1.0.0",
100
101
  "@cloudflare/vitest-plugin": "^1.0.0",
101
- "@cloudflare/workers-types": "^5.20260825.1",
102
+ "@cloudflare/workers-types": "^5.20260926.1",
102
103
  "@effect/sql-d1": "^4.0.0-rc.115",
103
104
  "@effect/sql-pg": "^4.0.0-rc.115",
104
105
  "@effect/sql-sqlite-do": "^4.0.0-rc.115",
@@ -0,0 +1,162 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Stream from "effect/Stream";
3
+
4
+ import { ContainerError } from "./DurableObjectContainer";
5
+
6
+ export type FileContent =
7
+ | string
8
+ | ArrayBuffer
9
+ | ArrayBufferView
10
+ | Blob
11
+ | ReadableStream<Uint8Array>;
12
+ export type FileType =
13
+ | "file"
14
+ | "directory"
15
+ | "symlink"
16
+ | "blockDevice"
17
+ | "characterDevice"
18
+ | "fifo"
19
+ | "socket";
20
+
21
+ export interface FileOperationOptions {
22
+ readonly cwd?: string;
23
+ readonly user?: string;
24
+ }
25
+
26
+ export interface MkdirOptions extends FileOperationOptions {
27
+ readonly recursive?: boolean;
28
+ }
29
+
30
+ export interface RemoveOptions extends FileOperationOptions {
31
+ readonly recursive?: boolean;
32
+ readonly force?: boolean;
33
+ }
34
+
35
+ export interface FileStat {
36
+ readonly type: FileType;
37
+ readonly size: bigint;
38
+ readonly mode: number;
39
+ readonly uid: number;
40
+ readonly gid: number;
41
+ readonly accessedAt: Date;
42
+ readonly modifiedAt: Date;
43
+ readonly changedAt: Date;
44
+ }
45
+
46
+ export interface DirectoryEntry {
47
+ readonly name: string;
48
+ readonly type: FileType;
49
+ }
50
+
51
+ type WithSignal<A> = A & { readonly signal?: AbortSignal };
52
+
53
+ /** Structural contract of Sandbox SDK 1.x `Files`, without loading an SDK version. */
54
+ export interface FilesResource {
55
+ readFile(path: string, options?: WithSignal<FileOperationOptions>): Promise<Response>;
56
+ writeFile(
57
+ path: string,
58
+ content: FileContent,
59
+ options?: WithSignal<FileOperationOptions>,
60
+ ): Promise<void>;
61
+ stat(path: string, options?: WithSignal<FileOperationOptions>): Promise<FileStat>;
62
+ lstat(path: string, options?: WithSignal<FileOperationOptions>): Promise<FileStat>;
63
+ readDirectory(
64
+ path: string,
65
+ options?: WithSignal<FileOperationOptions>,
66
+ ): Promise<ReadonlyArray<DirectoryEntry>>;
67
+ mkdir(path: string, options?: WithSignal<MkdirOptions>): Promise<void>;
68
+ rename(
69
+ source: string,
70
+ destination: string,
71
+ options?: WithSignal<FileOperationOptions>,
72
+ ): Promise<void>;
73
+ remove(path: string, options?: WithSignal<RemoveOptions>): Promise<void>;
74
+ }
75
+
76
+ export interface ContainerFiles {
77
+ readonly raw: FilesResource;
78
+ /** The caller consumes or cancels the response body. Late failures retain native SDK errors. */
79
+ readonly readFile: (
80
+ path: string,
81
+ options?: FileOperationOptions,
82
+ ) => Effect.Effect<Response, ContainerError>;
83
+ /** Includes failures that arrive while reading the response body. */
84
+ readonly readFileStream: (
85
+ path: string,
86
+ options?: FileOperationOptions,
87
+ ) => Stream.Stream<Uint8Array, ContainerError>;
88
+ readonly readFileString: (
89
+ path: string,
90
+ options?: FileOperationOptions,
91
+ ) => Effect.Effect<string, ContainerError>;
92
+ readonly writeFile: (
93
+ path: string,
94
+ content: FileContent,
95
+ options?: FileOperationOptions,
96
+ ) => Effect.Effect<void, ContainerError>;
97
+ readonly stat: (
98
+ path: string,
99
+ options?: FileOperationOptions,
100
+ ) => Effect.Effect<FileStat, ContainerError>;
101
+ readonly lstat: (
102
+ path: string,
103
+ options?: FileOperationOptions,
104
+ ) => Effect.Effect<FileStat, ContainerError>;
105
+ readonly readDirectory: (
106
+ path: string,
107
+ options?: FileOperationOptions,
108
+ ) => Effect.Effect<ReadonlyArray<DirectoryEntry>, ContainerError>;
109
+ readonly mkdir: (path: string, options?: MkdirOptions) => Effect.Effect<void, ContainerError>;
110
+ readonly rename: (
111
+ source: string,
112
+ destination: string,
113
+ options?: FileOperationOptions,
114
+ ) => Effect.Effect<void, ContainerError>;
115
+ readonly remove: (path: string, options?: RemoveOptions) => Effect.Effect<void, ContainerError>;
116
+ }
117
+
118
+ const attempt = <A>(operation: string, evaluate: (signal: AbortSignal) => Promise<A>) =>
119
+ Effect.tryPromise({
120
+ try: evaluate,
121
+ catch: (cause) => new ContainerError({ operation: `files.${operation}`, cause }),
122
+ });
123
+
124
+ /** Pass `new Files(container.raw)` from SDK 1.x. The image must supply its matching sandbox-shim. */
125
+ export const fromFiles = (files: FilesResource): ContainerFiles => {
126
+ const readFile = (path: string, options?: FileOperationOptions) =>
127
+ attempt("readFile", (signal) => files.readFile(path, { ...options, signal }));
128
+ const readFileStream = (path: string, options?: FileOperationOptions) =>
129
+ Stream.unwrap(
130
+ Effect.map(readFile(path, options), (response) => {
131
+ const body = response.body;
132
+
133
+ return body === null
134
+ ? Stream.empty
135
+ : Stream.fromReadableStream({
136
+ evaluate: () => body,
137
+ onError: (cause) => new ContainerError({ operation: "files.readFile", cause }),
138
+ });
139
+ }),
140
+ );
141
+
142
+ return {
143
+ raw: files,
144
+ readFile,
145
+ readFileStream,
146
+ readFileString: (path, options) =>
147
+ readFileStream(path, options).pipe(Stream.decodeText(), Stream.mkString),
148
+ writeFile: (path, content, options) =>
149
+ attempt("writeFile", (signal) => files.writeFile(path, content, { ...options, signal })),
150
+ stat: (path, options) => attempt("stat", (signal) => files.stat(path, { ...options, signal })),
151
+ lstat: (path, options) =>
152
+ attempt("lstat", (signal) => files.lstat(path, { ...options, signal })),
153
+ readDirectory: (path, options) =>
154
+ attempt("readDirectory", (signal) => files.readDirectory(path, { ...options, signal })),
155
+ mkdir: (path, options) =>
156
+ attempt("mkdir", (signal) => files.mkdir(path, { ...options, signal })),
157
+ rename: (source, destination, options) =>
158
+ attempt("rename", (signal) => files.rename(source, destination, { ...options, signal })),
159
+ remove: (path, options) =>
160
+ attempt("remove", (signal) => files.remove(path, { ...options, signal })),
161
+ };
162
+ };
@@ -60,10 +60,12 @@ export const ContainerState = S.Union([
60
60
  export type ContainerState = typeof ContainerState.Type;
61
61
 
62
62
  /**
63
- * Native Container stub shape exposed by a Container namespace binding.
63
+ * RPC stub exposed by the legacy `@cloudflare/containers` Container class.
64
64
  *
65
65
  * This is structural so using Container bindings does not make
66
66
  * `@cloudflare/containers` a runtime dependency of effect-cf.
67
+ * Use `DurableObjectContainer` inside a Durable Object for the native
68
+ * `ctx.container` API and the `durable_object` scheduling policy.
67
69
  */
68
70
  export interface ContainerStub {
69
71
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;