effect-cf 0.48.0 → 0.50.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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -2
- package/dist/ContainerFiles.d.mts +66 -0
- package/dist/ContainerFiles.mjs +66 -0
- package/dist/ContainerNamespace.d.mts +3 -1
- package/dist/DurableObjectContainer.d.mts +133 -0
- package/dist/DurableObjectContainer.mjs +179 -0
- package/dist/Sandbox.d.mts +1 -1
- package/dist/Sandbox.mjs +68 -64
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/package.json +4 -3
- package/src/ContainerFiles.ts +162 -0
- package/src/ContainerNamespace.ts +3 -1
- package/src/DurableObjectContainer.ts +388 -0
- package/src/Sandbox.ts +152 -98
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
+
## 0.50.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#197](https://github.com/danieljvdm/effect-cf/pull/197) [`e174201`](https://github.com/danieljvdm/effect-cf/commit/e174201015b89202988c1bac5a3b25f1bfa6f5d0) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add `DurableObjectContainer` for direct Effect access to `ctx.container`, including runtime image and instance selection, native process execution with scoped cleanup and output streams, filesystem snapshots, HTTP readiness, TCP/HTTP ports with preview proxying, outbound interception, and idle timeouts. Timeout and polling options accept Effect duration inputs. Supply its layer to `DurableObject.make`, or wrap a native container with `fromContainer`.
|
|
8
|
+
|
|
9
|
+
Add `ContainerFiles.fromFiles` for Effect file operations and streams backed by Sandbox SDK 1.x `Files` and its matching `sandbox-shim`. File and native container failures retain their original causes.
|
|
10
|
+
|
|
11
|
+
Update the tested Cloudflare baseline to workerd `1.20260926.1`, Workers types `5.20260926.1`, Miniflare `5.20260926.1-alpha`, and Wrangler `4.144.0`, with compatibility date `2026-09-26`. Consumers using the optional Workers types package should update to the supported range or regenerate their Wrangler runtime types.
|
|
12
|
+
|
|
13
|
+
Retain the legacy Container and Sandbox adapters and document migration to the native API. Allow Sandbox SDK 1.x utilities alongside the native adapter; `effect-cf/sandbox` continues to require the legacy SDK 0.13 API.
|
|
14
|
+
|
|
15
|
+
## 0.49.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#194](https://github.com/danieljvdm/effect-cf/pull/194) [`adf4e33`](https://github.com/danieljvdm/effect-cf/commit/adf4e339aaaef21ef2abb87595c07406a3e64ac3) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Reacquire sandbox RPC clients after a failed native channel. Retained sandbox instances now use a fresh channel for subsequent operations while preserving invocation-local channel reuse. Failed operations are still returned to the caller without automatic replay.
|
|
20
|
+
|
|
21
|
+
`SandboxInstanceClient.rawUnsafe` now includes `SandboxOperationError` because acquiring a fresh native client can fail.
|
|
22
|
+
|
|
3
23
|
## 0.48.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -6,12 +6,14 @@ Cloudflare entrypoints and bindings as Effect services.
|
|
|
6
6
|
npm install effect-cf "effect@^4.0.0-rc.115"
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
The repository tests against workerd `1.
|
|
9
|
+
The repository tests against workerd `1.20260926.1` and `@cloudflare/workers-types@5.20260926.1`. Use `compatibility_date: "2026-09-26"` in Wrangler.
|
|
10
10
|
|
|
11
11
|
See the [runtime performance guide](https://github.com/danieljvdm/effect-cf/blob/main/docs/runtime-performance.md) for measured alarm batching and telemetry configuration guidance.
|
|
12
12
|
|
|
13
13
|
For typed WebSocket RPC clients whose connection lifetime is owned by an Effect layer, see the [WebSocket RPC guide](https://github.com/danieljvdm/effect-cf/blob/main/docs/websocket-rpc.md).
|
|
14
14
|
|
|
15
|
+
`DurableObjectContainer.layer` adds native `ctx.container` control to `DurableObject.make`: runtime image selection, command execution, filesystem snapshots, networking, and idle timeouts. See the [container guide](https://github.com/danieljvdm/effect-cf/blob/main/docs/containers.md) for setup and migration from the legacy Container and Sandbox classes.
|
|
16
|
+
|
|
15
17
|
## Worker
|
|
16
18
|
|
|
17
19
|
```ts
|
|
@@ -108,7 +110,7 @@ Enable tracing in `wrangler.jsonc`:
|
|
|
108
110
|
|
|
109
111
|
```jsonc
|
|
110
112
|
{
|
|
111
|
-
"compatibility_date": "2026-
|
|
113
|
+
"compatibility_date": "2026-09-26",
|
|
112
114
|
"observability": {
|
|
113
115
|
"traces": { "enabled": true },
|
|
114
116
|
},
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ContainerError } from "./DurableObjectContainer.mjs";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
declare namespace ContainerFiles_d_exports {
|
|
5
|
+
export { ContainerFiles, DirectoryEntry, FileContent, FileOperationOptions, FileStat, FileType, FilesResource, MkdirOptions, RemoveOptions, fromFiles };
|
|
6
|
+
}
|
|
7
|
+
type FileContent = string | ArrayBuffer | ArrayBufferView | Blob | ReadableStream<Uint8Array>;
|
|
8
|
+
type FileType = "file" | "directory" | "symlink" | "blockDevice" | "characterDevice" | "fifo" | "socket";
|
|
9
|
+
interface FileOperationOptions {
|
|
10
|
+
readonly cwd?: string;
|
|
11
|
+
readonly user?: string;
|
|
12
|
+
}
|
|
13
|
+
interface MkdirOptions extends FileOperationOptions {
|
|
14
|
+
readonly recursive?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface RemoveOptions extends FileOperationOptions {
|
|
17
|
+
readonly recursive?: boolean;
|
|
18
|
+
readonly force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface FileStat {
|
|
21
|
+
readonly type: FileType;
|
|
22
|
+
readonly size: bigint;
|
|
23
|
+
readonly mode: number;
|
|
24
|
+
readonly uid: number;
|
|
25
|
+
readonly gid: number;
|
|
26
|
+
readonly accessedAt: Date;
|
|
27
|
+
readonly modifiedAt: Date;
|
|
28
|
+
readonly changedAt: Date;
|
|
29
|
+
}
|
|
30
|
+
interface DirectoryEntry {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly type: FileType;
|
|
33
|
+
}
|
|
34
|
+
type WithSignal<A> = A & {
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
};
|
|
37
|
+
/** Structural contract of Sandbox SDK 1.x `Files`, without loading an SDK version. */
|
|
38
|
+
interface FilesResource {
|
|
39
|
+
readFile(path: string, options?: WithSignal<FileOperationOptions>): Promise<Response>;
|
|
40
|
+
writeFile(path: string, content: FileContent, options?: WithSignal<FileOperationOptions>): Promise<void>;
|
|
41
|
+
stat(path: string, options?: WithSignal<FileOperationOptions>): Promise<FileStat>;
|
|
42
|
+
lstat(path: string, options?: WithSignal<FileOperationOptions>): Promise<FileStat>;
|
|
43
|
+
readDirectory(path: string, options?: WithSignal<FileOperationOptions>): Promise<ReadonlyArray<DirectoryEntry>>;
|
|
44
|
+
mkdir(path: string, options?: WithSignal<MkdirOptions>): Promise<void>;
|
|
45
|
+
rename(source: string, destination: string, options?: WithSignal<FileOperationOptions>): Promise<void>;
|
|
46
|
+
remove(path: string, options?: WithSignal<RemoveOptions>): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
interface ContainerFiles {
|
|
49
|
+
readonly raw: FilesResource;
|
|
50
|
+
/** The caller consumes or cancels the response body. Late failures retain native SDK errors. */
|
|
51
|
+
readonly readFile: (path: string, options?: FileOperationOptions) => Effect.Effect<Response, ContainerError>;
|
|
52
|
+
/** Includes failures that arrive while reading the response body. */
|
|
53
|
+
readonly readFileStream: (path: string, options?: FileOperationOptions) => Stream.Stream<Uint8Array, ContainerError>;
|
|
54
|
+
readonly readFileString: (path: string, options?: FileOperationOptions) => Effect.Effect<string, ContainerError>;
|
|
55
|
+
readonly writeFile: (path: string, content: FileContent, options?: FileOperationOptions) => Effect.Effect<void, ContainerError>;
|
|
56
|
+
readonly stat: (path: string, options?: FileOperationOptions) => Effect.Effect<FileStat, ContainerError>;
|
|
57
|
+
readonly lstat: (path: string, options?: FileOperationOptions) => Effect.Effect<FileStat, ContainerError>;
|
|
58
|
+
readonly readDirectory: (path: string, options?: FileOperationOptions) => Effect.Effect<ReadonlyArray<DirectoryEntry>, ContainerError>;
|
|
59
|
+
readonly mkdir: (path: string, options?: MkdirOptions) => Effect.Effect<void, ContainerError>;
|
|
60
|
+
readonly rename: (source: string, destination: string, options?: FileOperationOptions) => Effect.Effect<void, ContainerError>;
|
|
61
|
+
readonly remove: (path: string, options?: RemoveOptions) => Effect.Effect<void, ContainerError>;
|
|
62
|
+
}
|
|
63
|
+
/** Pass `new Files(container.raw)` from SDK 1.x. The image must supply its matching sandbox-shim. */
|
|
64
|
+
declare const fromFiles: (files: FilesResource) => ContainerFiles;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { ContainerFiles, ContainerFiles_d_exports, DirectoryEntry, FileContent, FileOperationOptions, FileStat, FileType, FilesResource, MkdirOptions, RemoveOptions, fromFiles };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { ContainerError } from "./DurableObjectContainer.mjs";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
//#region src/ContainerFiles.ts
|
|
6
|
+
var ContainerFiles_exports = /* @__PURE__ */ __exportAll({ fromFiles: () => fromFiles });
|
|
7
|
+
const attempt = (operation, evaluate) => Effect.tryPromise({
|
|
8
|
+
try: evaluate,
|
|
9
|
+
catch: (cause) => new ContainerError({
|
|
10
|
+
operation: `files.${operation}`,
|
|
11
|
+
cause
|
|
12
|
+
})
|
|
13
|
+
});
|
|
14
|
+
/** Pass `new Files(container.raw)` from SDK 1.x. The image must supply its matching sandbox-shim. */
|
|
15
|
+
const fromFiles = (files) => {
|
|
16
|
+
const readFile = (path, options) => attempt("readFile", (signal) => files.readFile(path, {
|
|
17
|
+
...options,
|
|
18
|
+
signal
|
|
19
|
+
}));
|
|
20
|
+
const readFileStream = (path, options) => Stream.unwrap(Effect.map(readFile(path, options), (response) => {
|
|
21
|
+
const body = response.body;
|
|
22
|
+
return body === null ? Stream.empty : Stream.fromReadableStream({
|
|
23
|
+
evaluate: () => body,
|
|
24
|
+
onError: (cause) => new ContainerError({
|
|
25
|
+
operation: "files.readFile",
|
|
26
|
+
cause
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
raw: files,
|
|
32
|
+
readFile,
|
|
33
|
+
readFileStream,
|
|
34
|
+
readFileString: (path, options) => readFileStream(path, options).pipe(Stream.decodeText(), Stream.mkString),
|
|
35
|
+
writeFile: (path, content, options) => attempt("writeFile", (signal) => files.writeFile(path, content, {
|
|
36
|
+
...options,
|
|
37
|
+
signal
|
|
38
|
+
})),
|
|
39
|
+
stat: (path, options) => attempt("stat", (signal) => files.stat(path, {
|
|
40
|
+
...options,
|
|
41
|
+
signal
|
|
42
|
+
})),
|
|
43
|
+
lstat: (path, options) => attempt("lstat", (signal) => files.lstat(path, {
|
|
44
|
+
...options,
|
|
45
|
+
signal
|
|
46
|
+
})),
|
|
47
|
+
readDirectory: (path, options) => attempt("readDirectory", (signal) => files.readDirectory(path, {
|
|
48
|
+
...options,
|
|
49
|
+
signal
|
|
50
|
+
})),
|
|
51
|
+
mkdir: (path, options) => attempt("mkdir", (signal) => files.mkdir(path, {
|
|
52
|
+
...options,
|
|
53
|
+
signal
|
|
54
|
+
})),
|
|
55
|
+
rename: (source, destination, options) => attempt("rename", (signal) => files.rename(source, destination, {
|
|
56
|
+
...options,
|
|
57
|
+
signal
|
|
58
|
+
})),
|
|
59
|
+
remove: (path, options) => attempt("remove", (signal) => files.remove(path, {
|
|
60
|
+
...options,
|
|
61
|
+
signal
|
|
62
|
+
}))
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
export { ContainerFiles_exports, fromFiles };
|
|
@@ -43,10 +43,12 @@ declare const ContainerState: S.Union<readonly [S.Struct<{
|
|
|
43
43
|
}>]>;
|
|
44
44
|
type ContainerState = typeof ContainerState.Type;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* RPC stub exposed by the legacy `@cloudflare/containers` Container class.
|
|
47
47
|
*
|
|
48
48
|
* This is structural so using Container bindings does not make
|
|
49
49
|
* `@cloudflare/containers` a runtime dependency of effect-cf.
|
|
50
|
+
* Use `DurableObjectContainer` inside a Durable Object for the native
|
|
51
|
+
* `ctx.container` API and the `durable_object` scheduling policy.
|
|
50
52
|
*/
|
|
51
53
|
interface ContainerStub {
|
|
52
54
|
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { DurableObjectState } from "./DurableObjectState.mjs";
|
|
2
|
+
import * as Context$1 from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer$1 from "effect/Layer";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Duration from "effect/Duration";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import { Scope } from "effect";
|
|
9
|
+
declare namespace DurableObjectContainer_d_exports {
|
|
10
|
+
export { ContainerError, ContainerExecOptions, ContainerExecOutput, ContainerExecOutputText, ContainerHttpReadinessOptions, ContainerInfo, ContainerNotConfiguredError, ContainerProcess, ContainerProcessLog, ContainerReadinessTimeoutError, ContainerSnapshot, ContainerSnapshotOptions, ContainerStartOptions, ContainerTcpPort, DurableObjectContainer, DurableObjectContainerService, fromContainer, layer };
|
|
11
|
+
}
|
|
12
|
+
type ContainerStartOptions = globalThis.ContainerStartupOptions;
|
|
13
|
+
type ContainerExecOptions = Omit<globalThis.ContainerExecOptions, "signal">;
|
|
14
|
+
type ContainerInfo = globalThis.ContainerInfo;
|
|
15
|
+
type ContainerSnapshot = globalThis.ContainerSnapshot;
|
|
16
|
+
type ContainerSnapshotOptions = globalThis.ContainerSnapshotOptions;
|
|
17
|
+
type ContainerExecOutput = globalThis.ExecOutput;
|
|
18
|
+
interface ContainerExecOutputText {
|
|
19
|
+
readonly stdout: string;
|
|
20
|
+
readonly stderr: string;
|
|
21
|
+
readonly exitCode: number;
|
|
22
|
+
}
|
|
23
|
+
interface ContainerProcessLog {
|
|
24
|
+
readonly stream: "stdout" | "stderr";
|
|
25
|
+
readonly data: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
interface ContainerHttpReadinessOptions {
|
|
28
|
+
/** Defaults to 30 seconds. All durations accept numbers in milliseconds. */
|
|
29
|
+
readonly timeout?: Duration.Input;
|
|
30
|
+
/** Delay between probes. Defaults to 100 milliseconds. */
|
|
31
|
+
readonly interval?: Duration.Input;
|
|
32
|
+
/** Bounds each connection or HTTP probe. Defaults to 1 second. */
|
|
33
|
+
readonly attemptTimeout?: Duration.Input;
|
|
34
|
+
readonly path?: string;
|
|
35
|
+
/** Accepts any HTTP response unless a status range is supplied. */
|
|
36
|
+
readonly status?: {
|
|
37
|
+
readonly min: number;
|
|
38
|
+
readonly max: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
declare const ContainerNotConfiguredError_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 & {
|
|
42
|
+
readonly _tag: "ContainerNotConfiguredError";
|
|
43
|
+
} & Readonly<A>;
|
|
44
|
+
declare class ContainerNotConfiguredError extends ContainerNotConfiguredError_base<{
|
|
45
|
+
readonly durableObjectId: string;
|
|
46
|
+
}> {
|
|
47
|
+
get message(): string;
|
|
48
|
+
}
|
|
49
|
+
declare const ContainerError_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 & {
|
|
50
|
+
readonly _tag: "ContainerError";
|
|
51
|
+
} & Readonly<A>;
|
|
52
|
+
declare class ContainerError extends ContainerError_base<{
|
|
53
|
+
readonly operation: string;
|
|
54
|
+
readonly cause: unknown;
|
|
55
|
+
}> {
|
|
56
|
+
get message(): string;
|
|
57
|
+
}
|
|
58
|
+
declare const ContainerReadinessTimeoutError_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 & {
|
|
59
|
+
readonly _tag: "ContainerReadinessTimeoutError";
|
|
60
|
+
} & Readonly<A>;
|
|
61
|
+
declare class ContainerReadinessTimeoutError extends ContainerReadinessTimeoutError_base<{
|
|
62
|
+
readonly port: number;
|
|
63
|
+
readonly timeout: Duration.Duration;
|
|
64
|
+
/** The last completed probe failure, when available. */
|
|
65
|
+
readonly cause: unknown;
|
|
66
|
+
}> {
|
|
67
|
+
get message(): string;
|
|
68
|
+
}
|
|
69
|
+
/** A native process handle owned by its creating request. Choose one output consumer. */
|
|
70
|
+
interface ContainerProcess {
|
|
71
|
+
readonly raw: globalThis.ExecProcess;
|
|
72
|
+
readonly pid: number;
|
|
73
|
+
readonly isPty: boolean;
|
|
74
|
+
readonly stdin: globalThis.ExecProcess["stdin"];
|
|
75
|
+
/** Empty when the corresponding native stream is absent. */
|
|
76
|
+
readonly stdout: Stream.Stream<Uint8Array, ContainerError>;
|
|
77
|
+
readonly stderr: Stream.Stream<Uint8Array, ContainerError>;
|
|
78
|
+
/** Drains both channels concurrently. Single-use, without buffering or replay. */
|
|
79
|
+
readonly logs: Stream.Stream<ContainerProcessLog, ContainerError>;
|
|
80
|
+
/** Nonzero exit codes are values, not Effect failures. */
|
|
81
|
+
readonly exitCode: Effect.Effect<number, ContainerError>;
|
|
82
|
+
readonly output: Effect.Effect<ContainerExecOutput, ContainerError>;
|
|
83
|
+
readonly outputText: Effect.Effect<ContainerExecOutputText, ContainerError>;
|
|
84
|
+
/** Signals the main process only; does nothing after its exit has been observed. */
|
|
85
|
+
readonly kill: (signal?: number) => Effect.Effect<void, ContainerError>;
|
|
86
|
+
readonly resize: (cols: number, rows: number) => Effect.Effect<void, ContainerError>;
|
|
87
|
+
}
|
|
88
|
+
interface ContainerTcpPort {
|
|
89
|
+
readonly raw: globalThis.Fetcher;
|
|
90
|
+
readonly fetch: (input: RequestInfo | URL, init?: RequestInit) => Effect.Effect<Response, ContainerError>;
|
|
91
|
+
/** Forwards an authorized preview request once, using the container's HTTP transport. */
|
|
92
|
+
readonly proxy: (request: Request) => Effect.Effect<Response, ContainerError>;
|
|
93
|
+
/** The caller owns the returned socket and must close it. */
|
|
94
|
+
readonly connect: (...args: Parameters<globalThis.Fetcher["connect"]>) => Effect.Effect<ReturnType<globalThis.Fetcher["connect"]>, ContainerError>;
|
|
95
|
+
}
|
|
96
|
+
/** Direct container control inside a Durable Object; lifecycle policy belongs to the application. */
|
|
97
|
+
interface DurableObjectContainerService {
|
|
98
|
+
readonly raw: globalThis.Container;
|
|
99
|
+
readonly running: Effect.Effect<boolean, ContainerError>;
|
|
100
|
+
readonly images: Effect.Effect<Readonly<Record<string, string>>, ContainerError>;
|
|
101
|
+
readonly inspect: Effect.Effect<Option.Option<ContainerInfo>, ContainerError>;
|
|
102
|
+
/** Validates options and initiates startup. It does not wait for readiness. */
|
|
103
|
+
readonly start: (options?: ContainerStartOptions) => Effect.Effect<void, ContainerError>;
|
|
104
|
+
/** Waits for the container to stop; failures retain the native cause, including its exitCode. */
|
|
105
|
+
readonly monitor: Effect.Effect<void, ContainerError>;
|
|
106
|
+
readonly destroy: (reason?: Parameters<globalThis.Container["destroy"]>[0]) => Effect.Effect<void, ContainerError>;
|
|
107
|
+
readonly signal: (signal: number) => Effect.Effect<void, ContainerError>;
|
|
108
|
+
/** Effect duration input: numbers are milliseconds; bigints are nanoseconds. */
|
|
109
|
+
readonly setInactivityTimeout: (duration: Duration.Input) => Effect.Effect<void, ContainerError>;
|
|
110
|
+
readonly getTcpPort: (port: number) => Effect.Effect<ContainerTcpPort, ContainerError>;
|
|
111
|
+
/** Sends HTTP GET probes to an already-started container. Never retries a caller's request. */
|
|
112
|
+
readonly waitForHttp: (port: number, options?: ContainerHttpReadinessOptions) => Effect.Effect<void, ContainerError | ContainerReadinessTimeoutError>;
|
|
113
|
+
/**
|
|
114
|
+
* Runs an executable with arguments, without a shell or automatic container startup.
|
|
115
|
+
* Interruption cancels acquisition. After acquisition, the caller owns the process.
|
|
116
|
+
* To observe it from later requests, redirect output to files and ignore the native pipes.
|
|
117
|
+
*/
|
|
118
|
+
readonly exec: (command: ReadonlyArray<string>, options?: ContainerExecOptions) => Effect.Effect<ContainerProcess, ContainerError>;
|
|
119
|
+
/** Sends SIGKILL to a still-running main process when the scope closes. */
|
|
120
|
+
readonly execScoped: (command: ReadonlyArray<string>, options?: ContainerExecOptions) => Effect.Effect<ContainerProcess, ContainerError, Scope.Scope>;
|
|
121
|
+
readonly snapshotContainer: (options?: ContainerSnapshotOptions) => Effect.Effect<ContainerSnapshot, ContainerError>;
|
|
122
|
+
readonly interceptOutboundHttp: (address: string, binding: globalThis.Fetcher) => Effect.Effect<void, ContainerError>;
|
|
123
|
+
readonly interceptAllOutboundHttp: (binding: globalThis.Fetcher) => Effect.Effect<void, ContainerError>;
|
|
124
|
+
readonly interceptOutboundHttps: (address: string, binding: globalThis.Fetcher) => Effect.Effect<void, ContainerError>;
|
|
125
|
+
}
|
|
126
|
+
declare const DurableObjectContainer_base: Context$1.ServiceClass<DurableObjectContainer, "effect-cf/DurableObjectContainer", DurableObjectContainerService>;
|
|
127
|
+
declare class DurableObjectContainer extends DurableObjectContainer_base {}
|
|
128
|
+
/** Wraps `ctx.container` without starting it or taking ownership of its lifecycle. */
|
|
129
|
+
declare const fromContainer: (container: globalThis.Container) => DurableObjectContainerService;
|
|
130
|
+
/** Supplies the container attached to the current Durable Object, without owning it. */
|
|
131
|
+
declare const layer: Layer$1.Layer<DurableObjectContainer, ContainerNotConfiguredError, DurableObjectState>;
|
|
132
|
+
//#endregion
|
|
133
|
+
export { ContainerError, ContainerExecOptions, ContainerExecOutput, ContainerExecOutputText, ContainerHttpReadinessOptions, ContainerInfo, ContainerNotConfiguredError, ContainerProcess, ContainerProcessLog, ContainerReadinessTimeoutError, ContainerSnapshot, ContainerSnapshotOptions, ContainerStartOptions, ContainerTcpPort, DurableObjectContainer, DurableObjectContainerService, DurableObjectContainer_d_exports, fromContainer, layer };
|
|
@@ -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 Duration from "effect/Duration";
|
|
10
|
+
import * as Stream from "effect/Stream";
|
|
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 };
|
package/dist/Sandbox.d.mts
CHANGED
|
@@ -115,7 +115,7 @@ interface SandboxTunnelsClient {
|
|
|
115
115
|
readonly destroy: (portOrInfo: number | TunnelInfo$1) => Effect.Effect<void, SandboxOperationError>;
|
|
116
116
|
}
|
|
117
117
|
interface SandboxInstanceClient {
|
|
118
|
-
readonly rawUnsafe: Effect.Effect<SandboxClientResource>;
|
|
118
|
+
readonly rawUnsafe: Effect.Effect<SandboxClientResource, SandboxOperationError>;
|
|
119
119
|
readonly exec: (command: SandboxCommand$1, options?: ExecOptions$1) => Effect.Effect<SandboxProcessHandle, SandboxOperationError>;
|
|
120
120
|
readonly getProcess: (id: string) => Effect.Effect<Option.Option<SandboxProcessHandle>, SandboxOperationError>;
|
|
121
121
|
readonly listProcesses: Effect.Effect<Array<ProcessStatus$1>, SandboxOperationError>;
|