├── README.md ├── ai ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── Dockerfile ├── README.md ├── container │ ├── go.mod │ ├── go.sum │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc ├── compression-workflows ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── Dockerfile ├── README.md ├── container │ ├── go.mod │ ├── go.sum │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc ├── compression ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── Dockerfile ├── container │ ├── go.mod │ ├── go.sum │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc ├── compute ├── .gitignore ├── Dockerfile ├── README.md ├── common │ ├── container.ts │ └── manager.ts ├── container_src │ ├── go.mod │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json └── wrangler.jsonc ├── http2 ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── Dockerfile ├── README.md ├── container │ ├── go.mod │ ├── go.sum │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc ├── load-balancer ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── Dockerfile ├── README.md ├── container │ ├── go.mod │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc ├── sqlite ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── Dockerfile ├── README.md ├── container │ ├── go.mod │ └── main.go ├── package.json ├── pnpm-lock.yaml ├── src │ └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc ├── terminal ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── README.md ├── host │ ├── package.json │ ├── pnpm-lock.yaml │ └── server.js ├── package.json ├── pnpm-lock.yaml ├── src │ ├── index.ts │ └── terminal.html └── wrangler.jsonc └── websockets ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── container ├── go.mod ├── go.sum └── main.go ├── package.json ├── pnpm-lock.yaml ├── src └── index.ts ├── tsconfig.json ├── worker-configuration.d.ts └── wrangler.jsonc /README.md: -------------------------------------------------------------------------------- 1 | # Durable Object Container examples 2 | 3 | This repository contains a set of directories with Durable Object Container examples. 4 | 5 | To deploy any of this examples you have to: 6 | ``` 7 | cd && pnpm install && pnpm run deploy 8 | ``` 9 | -------------------------------------------------------------------------------- /ai/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /ai/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /ai/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /ai/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /ai/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.24 AS build 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY container/go.mod ./ 9 | RUN go mod download 10 | 11 | # Copy container src 12 | COPY container/*.go ./ 13 | # Build 14 | RUN CGO_ENABLED=0 GOOS=linux go build -o /server 15 | 16 | FROM debian:latest 17 | 18 | RUN apt-get update && apt-get install -y python3 19 | 20 | COPY --from=build /server /server 21 | 22 | EXPOSE 8080 23 | # Run 24 | CMD ["/server"] 25 | -------------------------------------------------------------------------------- /ai/README.md: -------------------------------------------------------------------------------- 1 | # AI + Containers = Code execution? 2 | 3 | Send a POST request with some text to this worker, and you will automatically execute some code in a container generated by an AI. 4 | -------------------------------------------------------------------------------- /ai/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.24 4 | 5 | require ( 6 | golang.org/x/net v0.39.0 // indirect 7 | golang.org/x/text v0.24.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /ai/container/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 2 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 3 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 4 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 5 | -------------------------------------------------------------------------------- /ai/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | func handler(w http.ResponseWriter, r *http.Request) { 17 | country := os.Getenv("CLOUDFLARE_COUNTRY_A2") 18 | location := os.Getenv("CLOUDFLARE_LOCATION") 19 | region := os.Getenv("CLOUDFLARE_REGION") 20 | 21 | fmt.Fprintf(w, "Hi, I'm a container running in %s, %s, which is part of %s ", location, country, region) 22 | } 23 | 24 | func main() { 25 | c := make(chan os.Signal, 10) 26 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 27 | terminate := false 28 | go func() { 29 | for range c { 30 | if terminate { 31 | os.Exit(0) 32 | continue 33 | } 34 | 35 | terminate = true 36 | go func() { 37 | time.Sleep(time.Minute) 38 | os.Exit(0) 39 | }() 40 | } 41 | }() 42 | 43 | mux := http.NewServeMux() 44 | mux.HandleFunc("POST /execute", func(w http.ResponseWriter, r *http.Request) { 45 | cmd := exec.Command("python3") 46 | cmd.Stdin = r.Body 47 | b := &bytes.Buffer{} 48 | cmd.Stdout = b 49 | if err := cmd.Run(); err != nil { 50 | w.WriteHeader(400) 51 | } 52 | 53 | io.Copy(w, b) 54 | }) 55 | 56 | mux.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { 57 | if terminate { 58 | w.WriteHeader(400) 59 | w.Write([]byte("draining")) 60 | return 61 | } 62 | 63 | w.Write([]byte("ok")) 64 | }) 65 | 66 | server := &http.Server{ 67 | Addr: "0.0.0.0:8080", 68 | Handler: mux, 69 | } 70 | 71 | log.Fatal(server.ListenAndServe()) 72 | } 73 | -------------------------------------------------------------------------------- /ai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ai/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@cloudflare/workers-types': 12 | specifier: ^4.20250403.0 13 | version: 4.20250410.0 14 | typescript: 15 | specifier: ^5.5.2 16 | version: 5.8.3 17 | wrangler: 18 | specifier: ^4.14.4 19 | version: 4.14.4(@cloudflare/workers-types@4.20250410.0) 20 | 21 | packages: 22 | 23 | '@cloudflare/kv-asset-handler@0.4.0': 24 | resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} 25 | engines: {node: '>=18.0.0'} 26 | 27 | '@cloudflare/unenv-preset@2.3.1': 28 | resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==} 29 | peerDependencies: 30 | unenv: 2.0.0-rc.15 31 | workerd: ^1.20250320.0 32 | peerDependenciesMeta: 33 | workerd: 34 | optional: true 35 | 36 | '@cloudflare/workerd-darwin-64@1.20250507.0': 37 | resolution: {integrity: sha512-xC+8hmQuOUUNCVT9DWpLMfxhR4Xs4kI8v7Bkybh4pzGC85moH6fMfCBNaP0YQCNAA/BR56aL/AwfvMVGskTK/A==} 38 | engines: {node: '>=16'} 39 | cpu: [x64] 40 | os: [darwin] 41 | 42 | '@cloudflare/workerd-darwin-arm64@1.20250507.0': 43 | resolution: {integrity: sha512-Oynff5H8yM4trfUFaKdkOvPV3jac8mg7QC19ILZluCVgLx/JGEVLEJ7do1Na9rLqV8CK4gmUXPrUMX7uerhQgg==} 44 | engines: {node: '>=16'} 45 | cpu: [arm64] 46 | os: [darwin] 47 | 48 | '@cloudflare/workerd-linux-64@1.20250507.0': 49 | resolution: {integrity: sha512-/HAA+Zg/R7Q/Smyl835FUFKjotZN1UzN9j/BHBd0xKmKov97QkXAX8gsyGnyKqRReIOinp8x/8+UebTICR7VJw==} 50 | engines: {node: '>=16'} 51 | cpu: [x64] 52 | os: [linux] 53 | 54 | '@cloudflare/workerd-linux-arm64@1.20250507.0': 55 | resolution: {integrity: sha512-NMPibSdOYeycU0IrKkgOESFJQy7dEpHvuatZxQxlT+mIQK0INzI3irp2kKxhF99s25kPC4p+xg9bU3ugTrs3VQ==} 56 | engines: {node: '>=16'} 57 | cpu: [arm64] 58 | os: [linux] 59 | 60 | '@cloudflare/workerd-windows-64@1.20250507.0': 61 | resolution: {integrity: sha512-c91fhNP8ufycdIDqjVyKTqeb4ewkbAYXFQbLreMVgh4LLQQPDDEte8wCdmaFy5bIL0M9d85PpdCq51RCzq/FaQ==} 62 | engines: {node: '>=16'} 63 | cpu: [x64] 64 | os: [win32] 65 | 66 | '@cloudflare/workers-types@4.20250410.0': 67 | resolution: {integrity: sha512-Yx9VUi6QpmXtUIhOL+em+V02gue12kmVBVL6RGH5mhFh50M0x9JyOmm6wKwKZUny2uQd+22nuouE2q3z1OrsIQ==} 68 | 69 | '@cspotcode/source-map-support@0.8.1': 70 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 71 | engines: {node: '>=12'} 72 | 73 | '@emnapi/runtime@1.4.0': 74 | resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} 75 | 76 | '@esbuild/aix-ppc64@0.25.4': 77 | resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} 78 | engines: {node: '>=18'} 79 | cpu: [ppc64] 80 | os: [aix] 81 | 82 | '@esbuild/android-arm64@0.25.4': 83 | resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} 84 | engines: {node: '>=18'} 85 | cpu: [arm64] 86 | os: [android] 87 | 88 | '@esbuild/android-arm@0.25.4': 89 | resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} 90 | engines: {node: '>=18'} 91 | cpu: [arm] 92 | os: [android] 93 | 94 | '@esbuild/android-x64@0.25.4': 95 | resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} 96 | engines: {node: '>=18'} 97 | cpu: [x64] 98 | os: [android] 99 | 100 | '@esbuild/darwin-arm64@0.25.4': 101 | resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} 102 | engines: {node: '>=18'} 103 | cpu: [arm64] 104 | os: [darwin] 105 | 106 | '@esbuild/darwin-x64@0.25.4': 107 | resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} 108 | engines: {node: '>=18'} 109 | cpu: [x64] 110 | os: [darwin] 111 | 112 | '@esbuild/freebsd-arm64@0.25.4': 113 | resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} 114 | engines: {node: '>=18'} 115 | cpu: [arm64] 116 | os: [freebsd] 117 | 118 | '@esbuild/freebsd-x64@0.25.4': 119 | resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} 120 | engines: {node: '>=18'} 121 | cpu: [x64] 122 | os: [freebsd] 123 | 124 | '@esbuild/linux-arm64@0.25.4': 125 | resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} 126 | engines: {node: '>=18'} 127 | cpu: [arm64] 128 | os: [linux] 129 | 130 | '@esbuild/linux-arm@0.25.4': 131 | resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} 132 | engines: {node: '>=18'} 133 | cpu: [arm] 134 | os: [linux] 135 | 136 | '@esbuild/linux-ia32@0.25.4': 137 | resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} 138 | engines: {node: '>=18'} 139 | cpu: [ia32] 140 | os: [linux] 141 | 142 | '@esbuild/linux-loong64@0.25.4': 143 | resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} 144 | engines: {node: '>=18'} 145 | cpu: [loong64] 146 | os: [linux] 147 | 148 | '@esbuild/linux-mips64el@0.25.4': 149 | resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} 150 | engines: {node: '>=18'} 151 | cpu: [mips64el] 152 | os: [linux] 153 | 154 | '@esbuild/linux-ppc64@0.25.4': 155 | resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} 156 | engines: {node: '>=18'} 157 | cpu: [ppc64] 158 | os: [linux] 159 | 160 | '@esbuild/linux-riscv64@0.25.4': 161 | resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} 162 | engines: {node: '>=18'} 163 | cpu: [riscv64] 164 | os: [linux] 165 | 166 | '@esbuild/linux-s390x@0.25.4': 167 | resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} 168 | engines: {node: '>=18'} 169 | cpu: [s390x] 170 | os: [linux] 171 | 172 | '@esbuild/linux-x64@0.25.4': 173 | resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} 174 | engines: {node: '>=18'} 175 | cpu: [x64] 176 | os: [linux] 177 | 178 | '@esbuild/netbsd-arm64@0.25.4': 179 | resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} 180 | engines: {node: '>=18'} 181 | cpu: [arm64] 182 | os: [netbsd] 183 | 184 | '@esbuild/netbsd-x64@0.25.4': 185 | resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} 186 | engines: {node: '>=18'} 187 | cpu: [x64] 188 | os: [netbsd] 189 | 190 | '@esbuild/openbsd-arm64@0.25.4': 191 | resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} 192 | engines: {node: '>=18'} 193 | cpu: [arm64] 194 | os: [openbsd] 195 | 196 | '@esbuild/openbsd-x64@0.25.4': 197 | resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} 198 | engines: {node: '>=18'} 199 | cpu: [x64] 200 | os: [openbsd] 201 | 202 | '@esbuild/sunos-x64@0.25.4': 203 | resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} 204 | engines: {node: '>=18'} 205 | cpu: [x64] 206 | os: [sunos] 207 | 208 | '@esbuild/win32-arm64@0.25.4': 209 | resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} 210 | engines: {node: '>=18'} 211 | cpu: [arm64] 212 | os: [win32] 213 | 214 | '@esbuild/win32-ia32@0.25.4': 215 | resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} 216 | engines: {node: '>=18'} 217 | cpu: [ia32] 218 | os: [win32] 219 | 220 | '@esbuild/win32-x64@0.25.4': 221 | resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} 222 | engines: {node: '>=18'} 223 | cpu: [x64] 224 | os: [win32] 225 | 226 | '@fastify/busboy@2.1.1': 227 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 228 | engines: {node: '>=14'} 229 | 230 | '@img/sharp-darwin-arm64@0.33.5': 231 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 232 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 233 | cpu: [arm64] 234 | os: [darwin] 235 | 236 | '@img/sharp-darwin-x64@0.33.5': 237 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 238 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 239 | cpu: [x64] 240 | os: [darwin] 241 | 242 | '@img/sharp-libvips-darwin-arm64@1.0.4': 243 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 244 | cpu: [arm64] 245 | os: [darwin] 246 | 247 | '@img/sharp-libvips-darwin-x64@1.0.4': 248 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 249 | cpu: [x64] 250 | os: [darwin] 251 | 252 | '@img/sharp-libvips-linux-arm64@1.0.4': 253 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 254 | cpu: [arm64] 255 | os: [linux] 256 | 257 | '@img/sharp-libvips-linux-arm@1.0.5': 258 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 259 | cpu: [arm] 260 | os: [linux] 261 | 262 | '@img/sharp-libvips-linux-s390x@1.0.4': 263 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 264 | cpu: [s390x] 265 | os: [linux] 266 | 267 | '@img/sharp-libvips-linux-x64@1.0.4': 268 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 269 | cpu: [x64] 270 | os: [linux] 271 | 272 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 273 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 274 | cpu: [arm64] 275 | os: [linux] 276 | 277 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 278 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 279 | cpu: [x64] 280 | os: [linux] 281 | 282 | '@img/sharp-linux-arm64@0.33.5': 283 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 284 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 285 | cpu: [arm64] 286 | os: [linux] 287 | 288 | '@img/sharp-linux-arm@0.33.5': 289 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 290 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 291 | cpu: [arm] 292 | os: [linux] 293 | 294 | '@img/sharp-linux-s390x@0.33.5': 295 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 296 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 297 | cpu: [s390x] 298 | os: [linux] 299 | 300 | '@img/sharp-linux-x64@0.33.5': 301 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 302 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 303 | cpu: [x64] 304 | os: [linux] 305 | 306 | '@img/sharp-linuxmusl-arm64@0.33.5': 307 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 308 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 309 | cpu: [arm64] 310 | os: [linux] 311 | 312 | '@img/sharp-linuxmusl-x64@0.33.5': 313 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 314 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 315 | cpu: [x64] 316 | os: [linux] 317 | 318 | '@img/sharp-wasm32@0.33.5': 319 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 320 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 321 | cpu: [wasm32] 322 | 323 | '@img/sharp-win32-ia32@0.33.5': 324 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 325 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 326 | cpu: [ia32] 327 | os: [win32] 328 | 329 | '@img/sharp-win32-x64@0.33.5': 330 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 331 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 332 | cpu: [x64] 333 | os: [win32] 334 | 335 | '@jridgewell/resolve-uri@3.1.2': 336 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 337 | engines: {node: '>=6.0.0'} 338 | 339 | '@jridgewell/sourcemap-codec@1.5.0': 340 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 341 | 342 | '@jridgewell/trace-mapping@0.3.9': 343 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 344 | 345 | acorn-walk@8.3.2: 346 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 347 | engines: {node: '>=0.4.0'} 348 | 349 | acorn@8.14.0: 350 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 351 | engines: {node: '>=0.4.0'} 352 | hasBin: true 353 | 354 | as-table@1.0.55: 355 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 356 | 357 | blake3-wasm@2.1.5: 358 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 359 | 360 | color-convert@2.0.1: 361 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 362 | engines: {node: '>=7.0.0'} 363 | 364 | color-name@1.1.4: 365 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 366 | 367 | color-string@1.9.1: 368 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 369 | 370 | color@4.2.3: 371 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 372 | engines: {node: '>=12.5.0'} 373 | 374 | cookie@0.7.2: 375 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 376 | engines: {node: '>= 0.6'} 377 | 378 | data-uri-to-buffer@2.0.2: 379 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 380 | 381 | defu@6.1.4: 382 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 383 | 384 | detect-libc@2.0.3: 385 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 386 | engines: {node: '>=8'} 387 | 388 | esbuild@0.25.4: 389 | resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} 390 | engines: {node: '>=18'} 391 | hasBin: true 392 | 393 | exit-hook@2.2.1: 394 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 395 | engines: {node: '>=6'} 396 | 397 | exsolve@1.0.4: 398 | resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} 399 | 400 | fsevents@2.3.3: 401 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 402 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 403 | os: [darwin] 404 | 405 | get-source@2.0.12: 406 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 407 | 408 | glob-to-regexp@0.4.1: 409 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 410 | 411 | is-arrayish@0.3.2: 412 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 413 | 414 | mime@3.0.0: 415 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 416 | engines: {node: '>=10.0.0'} 417 | hasBin: true 418 | 419 | miniflare@4.20250507.0: 420 | resolution: {integrity: sha512-EgbQRt/Hnr8HCmW2J/4LRNE3yOzJTdNd98XJ8gnGXFKcimXxUFPiWP3k1df+ZPCtEHp6cXxi8+jP7v9vuIbIsg==} 421 | engines: {node: '>=18.0.0'} 422 | hasBin: true 423 | 424 | mustache@4.2.0: 425 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 426 | hasBin: true 427 | 428 | ohash@2.0.11: 429 | resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} 430 | 431 | path-to-regexp@6.3.0: 432 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 433 | 434 | pathe@2.0.3: 435 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 436 | 437 | printable-characters@1.0.42: 438 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 439 | 440 | semver@7.7.1: 441 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 442 | engines: {node: '>=10'} 443 | hasBin: true 444 | 445 | sharp@0.33.5: 446 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 447 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 448 | 449 | simple-swizzle@0.2.2: 450 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 451 | 452 | source-map@0.6.1: 453 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 454 | engines: {node: '>=0.10.0'} 455 | 456 | stacktracey@2.1.8: 457 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 458 | 459 | stoppable@1.1.0: 460 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 461 | engines: {node: '>=4', npm: '>=6'} 462 | 463 | tslib@2.8.1: 464 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 465 | 466 | typescript@5.8.3: 467 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 468 | engines: {node: '>=14.17'} 469 | hasBin: true 470 | 471 | ufo@1.6.1: 472 | resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 473 | 474 | undici@5.29.0: 475 | resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} 476 | engines: {node: '>=14.0'} 477 | 478 | unenv@2.0.0-rc.15: 479 | resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} 480 | 481 | workerd@1.20250507.0: 482 | resolution: {integrity: sha512-OXaGjEh5THT9iblwWIyPrYBoaPe/d4zN03Go7/w8CmS8sma7//O9hjbk43sboWkc89taGPmU0/LNyZUUiUlHeQ==} 483 | engines: {node: '>=16'} 484 | hasBin: true 485 | 486 | wrangler@4.14.4: 487 | resolution: {integrity: sha512-HIdOdiMIcJV5ymw80RKsr3Uzen/p1kRX4jnCEmR2XVeoEhV2Qw6GABxS5WMTlSES2/vEX0Y+ezUAdsprcUhJ5g==} 488 | engines: {node: '>=18.0.0'} 489 | hasBin: true 490 | peerDependencies: 491 | '@cloudflare/workers-types': ^4.20250507.0 492 | peerDependenciesMeta: 493 | '@cloudflare/workers-types': 494 | optional: true 495 | 496 | ws@8.18.0: 497 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 498 | engines: {node: '>=10.0.0'} 499 | peerDependencies: 500 | bufferutil: ^4.0.1 501 | utf-8-validate: '>=5.0.2' 502 | peerDependenciesMeta: 503 | bufferutil: 504 | optional: true 505 | utf-8-validate: 506 | optional: true 507 | 508 | youch@3.3.4: 509 | resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 510 | 511 | zod@3.22.3: 512 | resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 513 | 514 | snapshots: 515 | 516 | '@cloudflare/kv-asset-handler@0.4.0': 517 | dependencies: 518 | mime: 3.0.0 519 | 520 | '@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250507.0)': 521 | dependencies: 522 | unenv: 2.0.0-rc.15 523 | optionalDependencies: 524 | workerd: 1.20250507.0 525 | 526 | '@cloudflare/workerd-darwin-64@1.20250507.0': 527 | optional: true 528 | 529 | '@cloudflare/workerd-darwin-arm64@1.20250507.0': 530 | optional: true 531 | 532 | '@cloudflare/workerd-linux-64@1.20250507.0': 533 | optional: true 534 | 535 | '@cloudflare/workerd-linux-arm64@1.20250507.0': 536 | optional: true 537 | 538 | '@cloudflare/workerd-windows-64@1.20250507.0': 539 | optional: true 540 | 541 | '@cloudflare/workers-types@4.20250410.0': {} 542 | 543 | '@cspotcode/source-map-support@0.8.1': 544 | dependencies: 545 | '@jridgewell/trace-mapping': 0.3.9 546 | 547 | '@emnapi/runtime@1.4.0': 548 | dependencies: 549 | tslib: 2.8.1 550 | optional: true 551 | 552 | '@esbuild/aix-ppc64@0.25.4': 553 | optional: true 554 | 555 | '@esbuild/android-arm64@0.25.4': 556 | optional: true 557 | 558 | '@esbuild/android-arm@0.25.4': 559 | optional: true 560 | 561 | '@esbuild/android-x64@0.25.4': 562 | optional: true 563 | 564 | '@esbuild/darwin-arm64@0.25.4': 565 | optional: true 566 | 567 | '@esbuild/darwin-x64@0.25.4': 568 | optional: true 569 | 570 | '@esbuild/freebsd-arm64@0.25.4': 571 | optional: true 572 | 573 | '@esbuild/freebsd-x64@0.25.4': 574 | optional: true 575 | 576 | '@esbuild/linux-arm64@0.25.4': 577 | optional: true 578 | 579 | '@esbuild/linux-arm@0.25.4': 580 | optional: true 581 | 582 | '@esbuild/linux-ia32@0.25.4': 583 | optional: true 584 | 585 | '@esbuild/linux-loong64@0.25.4': 586 | optional: true 587 | 588 | '@esbuild/linux-mips64el@0.25.4': 589 | optional: true 590 | 591 | '@esbuild/linux-ppc64@0.25.4': 592 | optional: true 593 | 594 | '@esbuild/linux-riscv64@0.25.4': 595 | optional: true 596 | 597 | '@esbuild/linux-s390x@0.25.4': 598 | optional: true 599 | 600 | '@esbuild/linux-x64@0.25.4': 601 | optional: true 602 | 603 | '@esbuild/netbsd-arm64@0.25.4': 604 | optional: true 605 | 606 | '@esbuild/netbsd-x64@0.25.4': 607 | optional: true 608 | 609 | '@esbuild/openbsd-arm64@0.25.4': 610 | optional: true 611 | 612 | '@esbuild/openbsd-x64@0.25.4': 613 | optional: true 614 | 615 | '@esbuild/sunos-x64@0.25.4': 616 | optional: true 617 | 618 | '@esbuild/win32-arm64@0.25.4': 619 | optional: true 620 | 621 | '@esbuild/win32-ia32@0.25.4': 622 | optional: true 623 | 624 | '@esbuild/win32-x64@0.25.4': 625 | optional: true 626 | 627 | '@fastify/busboy@2.1.1': {} 628 | 629 | '@img/sharp-darwin-arm64@0.33.5': 630 | optionalDependencies: 631 | '@img/sharp-libvips-darwin-arm64': 1.0.4 632 | optional: true 633 | 634 | '@img/sharp-darwin-x64@0.33.5': 635 | optionalDependencies: 636 | '@img/sharp-libvips-darwin-x64': 1.0.4 637 | optional: true 638 | 639 | '@img/sharp-libvips-darwin-arm64@1.0.4': 640 | optional: true 641 | 642 | '@img/sharp-libvips-darwin-x64@1.0.4': 643 | optional: true 644 | 645 | '@img/sharp-libvips-linux-arm64@1.0.4': 646 | optional: true 647 | 648 | '@img/sharp-libvips-linux-arm@1.0.5': 649 | optional: true 650 | 651 | '@img/sharp-libvips-linux-s390x@1.0.4': 652 | optional: true 653 | 654 | '@img/sharp-libvips-linux-x64@1.0.4': 655 | optional: true 656 | 657 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 658 | optional: true 659 | 660 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 661 | optional: true 662 | 663 | '@img/sharp-linux-arm64@0.33.5': 664 | optionalDependencies: 665 | '@img/sharp-libvips-linux-arm64': 1.0.4 666 | optional: true 667 | 668 | '@img/sharp-linux-arm@0.33.5': 669 | optionalDependencies: 670 | '@img/sharp-libvips-linux-arm': 1.0.5 671 | optional: true 672 | 673 | '@img/sharp-linux-s390x@0.33.5': 674 | optionalDependencies: 675 | '@img/sharp-libvips-linux-s390x': 1.0.4 676 | optional: true 677 | 678 | '@img/sharp-linux-x64@0.33.5': 679 | optionalDependencies: 680 | '@img/sharp-libvips-linux-x64': 1.0.4 681 | optional: true 682 | 683 | '@img/sharp-linuxmusl-arm64@0.33.5': 684 | optionalDependencies: 685 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 686 | optional: true 687 | 688 | '@img/sharp-linuxmusl-x64@0.33.5': 689 | optionalDependencies: 690 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 691 | optional: true 692 | 693 | '@img/sharp-wasm32@0.33.5': 694 | dependencies: 695 | '@emnapi/runtime': 1.4.0 696 | optional: true 697 | 698 | '@img/sharp-win32-ia32@0.33.5': 699 | optional: true 700 | 701 | '@img/sharp-win32-x64@0.33.5': 702 | optional: true 703 | 704 | '@jridgewell/resolve-uri@3.1.2': {} 705 | 706 | '@jridgewell/sourcemap-codec@1.5.0': {} 707 | 708 | '@jridgewell/trace-mapping@0.3.9': 709 | dependencies: 710 | '@jridgewell/resolve-uri': 3.1.2 711 | '@jridgewell/sourcemap-codec': 1.5.0 712 | 713 | acorn-walk@8.3.2: {} 714 | 715 | acorn@8.14.0: {} 716 | 717 | as-table@1.0.55: 718 | dependencies: 719 | printable-characters: 1.0.42 720 | 721 | blake3-wasm@2.1.5: {} 722 | 723 | color-convert@2.0.1: 724 | dependencies: 725 | color-name: 1.1.4 726 | optional: true 727 | 728 | color-name@1.1.4: 729 | optional: true 730 | 731 | color-string@1.9.1: 732 | dependencies: 733 | color-name: 1.1.4 734 | simple-swizzle: 0.2.2 735 | optional: true 736 | 737 | color@4.2.3: 738 | dependencies: 739 | color-convert: 2.0.1 740 | color-string: 1.9.1 741 | optional: true 742 | 743 | cookie@0.7.2: {} 744 | 745 | data-uri-to-buffer@2.0.2: {} 746 | 747 | defu@6.1.4: {} 748 | 749 | detect-libc@2.0.3: 750 | optional: true 751 | 752 | esbuild@0.25.4: 753 | optionalDependencies: 754 | '@esbuild/aix-ppc64': 0.25.4 755 | '@esbuild/android-arm': 0.25.4 756 | '@esbuild/android-arm64': 0.25.4 757 | '@esbuild/android-x64': 0.25.4 758 | '@esbuild/darwin-arm64': 0.25.4 759 | '@esbuild/darwin-x64': 0.25.4 760 | '@esbuild/freebsd-arm64': 0.25.4 761 | '@esbuild/freebsd-x64': 0.25.4 762 | '@esbuild/linux-arm': 0.25.4 763 | '@esbuild/linux-arm64': 0.25.4 764 | '@esbuild/linux-ia32': 0.25.4 765 | '@esbuild/linux-loong64': 0.25.4 766 | '@esbuild/linux-mips64el': 0.25.4 767 | '@esbuild/linux-ppc64': 0.25.4 768 | '@esbuild/linux-riscv64': 0.25.4 769 | '@esbuild/linux-s390x': 0.25.4 770 | '@esbuild/linux-x64': 0.25.4 771 | '@esbuild/netbsd-arm64': 0.25.4 772 | '@esbuild/netbsd-x64': 0.25.4 773 | '@esbuild/openbsd-arm64': 0.25.4 774 | '@esbuild/openbsd-x64': 0.25.4 775 | '@esbuild/sunos-x64': 0.25.4 776 | '@esbuild/win32-arm64': 0.25.4 777 | '@esbuild/win32-ia32': 0.25.4 778 | '@esbuild/win32-x64': 0.25.4 779 | 780 | exit-hook@2.2.1: {} 781 | 782 | exsolve@1.0.4: {} 783 | 784 | fsevents@2.3.3: 785 | optional: true 786 | 787 | get-source@2.0.12: 788 | dependencies: 789 | data-uri-to-buffer: 2.0.2 790 | source-map: 0.6.1 791 | 792 | glob-to-regexp@0.4.1: {} 793 | 794 | is-arrayish@0.3.2: 795 | optional: true 796 | 797 | mime@3.0.0: {} 798 | 799 | miniflare@4.20250507.0: 800 | dependencies: 801 | '@cspotcode/source-map-support': 0.8.1 802 | acorn: 8.14.0 803 | acorn-walk: 8.3.2 804 | exit-hook: 2.2.1 805 | glob-to-regexp: 0.4.1 806 | stoppable: 1.1.0 807 | undici: 5.29.0 808 | workerd: 1.20250507.0 809 | ws: 8.18.0 810 | youch: 3.3.4 811 | zod: 3.22.3 812 | transitivePeerDependencies: 813 | - bufferutil 814 | - utf-8-validate 815 | 816 | mustache@4.2.0: {} 817 | 818 | ohash@2.0.11: {} 819 | 820 | path-to-regexp@6.3.0: {} 821 | 822 | pathe@2.0.3: {} 823 | 824 | printable-characters@1.0.42: {} 825 | 826 | semver@7.7.1: 827 | optional: true 828 | 829 | sharp@0.33.5: 830 | dependencies: 831 | color: 4.2.3 832 | detect-libc: 2.0.3 833 | semver: 7.7.1 834 | optionalDependencies: 835 | '@img/sharp-darwin-arm64': 0.33.5 836 | '@img/sharp-darwin-x64': 0.33.5 837 | '@img/sharp-libvips-darwin-arm64': 1.0.4 838 | '@img/sharp-libvips-darwin-x64': 1.0.4 839 | '@img/sharp-libvips-linux-arm': 1.0.5 840 | '@img/sharp-libvips-linux-arm64': 1.0.4 841 | '@img/sharp-libvips-linux-s390x': 1.0.4 842 | '@img/sharp-libvips-linux-x64': 1.0.4 843 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 844 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 845 | '@img/sharp-linux-arm': 0.33.5 846 | '@img/sharp-linux-arm64': 0.33.5 847 | '@img/sharp-linux-s390x': 0.33.5 848 | '@img/sharp-linux-x64': 0.33.5 849 | '@img/sharp-linuxmusl-arm64': 0.33.5 850 | '@img/sharp-linuxmusl-x64': 0.33.5 851 | '@img/sharp-wasm32': 0.33.5 852 | '@img/sharp-win32-ia32': 0.33.5 853 | '@img/sharp-win32-x64': 0.33.5 854 | optional: true 855 | 856 | simple-swizzle@0.2.2: 857 | dependencies: 858 | is-arrayish: 0.3.2 859 | optional: true 860 | 861 | source-map@0.6.1: {} 862 | 863 | stacktracey@2.1.8: 864 | dependencies: 865 | as-table: 1.0.55 866 | get-source: 2.0.12 867 | 868 | stoppable@1.1.0: {} 869 | 870 | tslib@2.8.1: 871 | optional: true 872 | 873 | typescript@5.8.3: {} 874 | 875 | ufo@1.6.1: {} 876 | 877 | undici@5.29.0: 878 | dependencies: 879 | '@fastify/busboy': 2.1.1 880 | 881 | unenv@2.0.0-rc.15: 882 | dependencies: 883 | defu: 6.1.4 884 | exsolve: 1.0.4 885 | ohash: 2.0.11 886 | pathe: 2.0.3 887 | ufo: 1.6.1 888 | 889 | workerd@1.20250507.0: 890 | optionalDependencies: 891 | '@cloudflare/workerd-darwin-64': 1.20250507.0 892 | '@cloudflare/workerd-darwin-arm64': 1.20250507.0 893 | '@cloudflare/workerd-linux-64': 1.20250507.0 894 | '@cloudflare/workerd-linux-arm64': 1.20250507.0 895 | '@cloudflare/workerd-windows-64': 1.20250507.0 896 | 897 | wrangler@4.14.4(@cloudflare/workers-types@4.20250410.0): 898 | dependencies: 899 | '@cloudflare/kv-asset-handler': 0.4.0 900 | '@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250507.0) 901 | blake3-wasm: 2.1.5 902 | esbuild: 0.25.4 903 | miniflare: 4.20250507.0 904 | path-to-regexp: 6.3.0 905 | unenv: 2.0.0-rc.15 906 | workerd: 1.20250507.0 907 | optionalDependencies: 908 | '@cloudflare/workers-types': 4.20250410.0 909 | fsevents: 2.3.3 910 | sharp: 0.33.5 911 | transitivePeerDependencies: 912 | - bufferutil 913 | - utf-8-validate 914 | 915 | ws@8.18.0: {} 916 | 917 | youch@3.3.4: 918 | dependencies: 919 | cookie: 0.7.2 920 | mustache: 4.2.0 921 | stacktracey: 2.1.8 922 | 923 | zod@3.22.3: {} 924 | -------------------------------------------------------------------------------- /ai/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from 'cloudflare:workers'; 2 | 3 | export class CodeExecutor extends DurableObject { 4 | container: globalThis.Container; 5 | monitor?: Promise; 6 | 7 | constructor(ctx: DurableObjectState, env: Env) { 8 | super(ctx, env); 9 | this.container = ctx.container!; 10 | void this.ctx.blockConcurrencyWhile(async () => { 11 | if (!this.container.running) this.container.start(); 12 | }); 13 | } 14 | 15 | async fetch(req: Request) { 16 | const prompt = await req.text(); 17 | const res = await this.env.AI.run('@cf/meta/llama-4-scout-17b-16e-instruct', { 18 | messages: [ 19 | { 20 | role: 'system', 21 | content: 22 | 'You will receive a second message with some python3 code, just generate python3 code that does not require any pip dependencies if possible. The python3 code should be clean so it can be piped through stdin to python3 and executed. Also do not use ```', 23 | }, 24 | { 25 | content: prompt, 26 | role: 'user', 27 | }, 28 | ], 29 | }); 30 | const { response } = res; 31 | return await this.container.getTcpPort(8080).fetch('http://container.com/execute', { method: 'POST', body: response }); 32 | } 33 | } 34 | 35 | export default { 36 | async fetch(request, env): Promise { 37 | try { 38 | return await env.CODE_EXECUTOR.get(env.CODE_EXECUTOR.idFromName('executor')).fetch(request); 39 | } catch (err) { 40 | console.error('Error fetch:', err.message); 41 | return new Response(err.message, { status: 500 }); 42 | } 43 | }, 44 | } satisfies ExportedHandler; 45 | -------------------------------------------------------------------------------- /ai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ai/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "ai-container", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "containers": [{ 7 | "name": "code-executor", 8 | "image": "./Dockerfile", 9 | "class_name": "CodeExecutor", 10 | "max_instances": 2 11 | }], 12 | "durable_objects": { 13 | "bindings": [ 14 | { 15 | "class_name": "CodeExecutor", 16 | "name": "CODE_EXECUTOR" 17 | }, 18 | ] 19 | }, 20 | "migrations": [ 21 | { 22 | "new_sqlite_classes": [ 23 | "CodeExecutor", 24 | ], 25 | "tag": "v1" 26 | } 27 | ], 28 | "observability": { 29 | "enabled": true 30 | }, 31 | "ai": { 32 | "binding": "AI" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /compression-workflows/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /compression-workflows/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /compression-workflows/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /compression-workflows/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /compression-workflows/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.23 AS build 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY container/go.mod container/go.sum ./ 9 | RUN go mod download 10 | 11 | # Copy container src 12 | COPY container/*.go ./ 13 | # Build 14 | RUN CGO_ENABLED=0 GOOS=linux go build -o /server 15 | 16 | FROM scratch 17 | COPY --from=build /server /server 18 | EXPOSE 8080 19 | # Run 20 | CMD ["/server"] 21 | -------------------------------------------------------------------------------- /compression-workflows/README.md: -------------------------------------------------------------------------------- 1 | # Container + Workflows + R2 2 | 3 | You need to have access to R2 and Workflows to deploy this. 4 | 5 | It's an example on how you can upload an object to R2, and then compress it in a container thanks to Workflows. 6 | -------------------------------------------------------------------------------- /compression-workflows/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23 4 | 5 | require github.com/klauspost/compress v1.18.0 6 | -------------------------------------------------------------------------------- /compression-workflows/container/go.sum: -------------------------------------------------------------------------------- 1 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 2 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 3 | -------------------------------------------------------------------------------- /compression-workflows/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/klauspost/compress/zstd" 10 | ) 11 | 12 | var logs bytes.Buffer 13 | 14 | func main() { 15 | requests := make(chan io.Reader, 10) 16 | http.HandleFunc("GET /compressions", func(w http.ResponseWriter, r *http.Request) { 17 | select { 18 | case r := <-requests: 19 | io.Copy(w, r) 20 | case <-r.Context().Done(): 21 | return 22 | } 23 | }) 24 | 25 | http.HandleFunc("PUT /compressions", func(w http.ResponseWriter, r *http.Request) { 26 | response, writer := io.Pipe() 27 | zstdWriter, err := zstd.NewWriter(writer, zstd.WithEncoderConcurrency(1)) 28 | if err != nil { 29 | w.WriteHeader(500) 30 | logs.WriteString("error getting writer: " + err.Error()) 31 | logs.WriteByte('\n') 32 | return 33 | } 34 | 35 | select { 36 | case requests <- response: 37 | default: 38 | w.WriteHeader(500) 39 | w.Write([]byte("server is overwhelmed")) 40 | return 41 | } 42 | 43 | n, err := io.Copy(zstdWriter, r.Body) 44 | fmt.Println("copied", n, err) 45 | zstdWriter.Close() 46 | writer.Close() 47 | w.WriteHeader(200) 48 | }) 49 | 50 | http.ListenAndServe(":8002", nil) 51 | } 52 | -------------------------------------------------------------------------------- /compression-workflows/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compression-workflows", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /compression-workflows/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject, WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from 'cloudflare:workers'; 2 | 3 | type Params = { 4 | r2Path: string; 5 | }; 6 | 7 | export function sleep(ms: number) { 8 | return new Promise((res) => setTimeout(res, ms)); 9 | } 10 | 11 | export class CompressorWorkflow extends WorkflowEntrypoint { 12 | async run(event: WorkflowEvent, step: WorkflowStep) { 13 | const container = this.env.COMPRESSOR.get(this.env.COMPRESSOR.idFromName(event.instanceId)); 14 | 15 | await step.do('wait for container to be healthy and pass r2 object, and put on another object', async () => { 16 | const tries = 10; 17 | await container.init(); 18 | 19 | const waitUntilContainerIsOk = async () => { 20 | let lastErr: unknown; 21 | for (let i = 0; i < tries; i++) { 22 | try { 23 | await container.logs(); 24 | return; 25 | } catch (err) { 26 | console.error('transient error:', err instanceof Error ? err.message : JSON.stringify(err)); 27 | await sleep(500); 28 | lastErr = err; 29 | } 30 | } 31 | 32 | throw lastErr; 33 | }; 34 | 35 | await waitUntilContainerIsOk(); 36 | 37 | const object = await this.env.COMPRESSOR_BUCKET.get(event.payload.r2Path); 38 | if (object === null) { 39 | console.error('Object not found: ' + event.payload.r2Path); 40 | return; 41 | } 42 | 43 | try { 44 | const result = await container.fetch(new Request('http://compressor', { method: 'POST', body: object.body })); 45 | await this.env.COMPRESSOR_BUCKET.put(`results${event.payload.r2Path}`, result.body); 46 | } catch (err) { 47 | console.error('There was an error compressing the object', err instanceof Error ? err.message : JSON.stringify(err)); 48 | throw err; 49 | } 50 | }); 51 | 52 | await step.do('destroy', async () => { 53 | await container.destroy(); 54 | }); 55 | } 56 | } 57 | 58 | export class Compressor extends DurableObject { 59 | container: Container; 60 | monitor?: Promise; 61 | 62 | constructor(ctx: DurableObjectState, env: Env) { 63 | super(ctx, env); 64 | if (ctx.container === undefined) throw new Error('no container'); 65 | this.container = ctx.container; 66 | ctx.blockConcurrencyWhile(async () => { 67 | if (!this.container.running) this.container.start({ entrypoint: ['/server'], enableInternet: false }); 68 | this.monitor = this.container.monitor().then(() => console.log('Container exited?')); 69 | }); 70 | } 71 | 72 | async init() { 73 | console.log('Starting container'); 74 | } 75 | 76 | async logs() { 77 | return await this.container.getTcpPort(8002).fetch('http://container'); 78 | } 79 | 80 | async destroy() { 81 | await this.ctx.container?.destroy(); 82 | await this.ctx.storage.deleteAll(); 83 | await this.ctx.storage.deleteAlarm(); 84 | await this.ctx.storage.sync(); 85 | this.ctx.abort(); 86 | } 87 | 88 | async fetch(req: Request): Promise { 89 | void this.container.getTcpPort(8002).fetch('http://container/compressions', { method: 'PUT', body: req.body }); 90 | return await this.container.getTcpPort(8002).fetch('http://container/compressions'); 91 | } 92 | } 93 | 94 | export default { 95 | async fetch(request, env): Promise { 96 | const stub = env.COMPRESSOR.get(env.COMPRESSOR.idFromName('compressor')); 97 | await stub.init(); 98 | 99 | if (request.method === 'POST') { 100 | try { 101 | return await stub.fetch(request); 102 | } catch (err) { 103 | return new Response(err instanceof Error ? err.message : JSON.stringify(err), { status: 500 }); 104 | } 105 | } 106 | 107 | if (request.method === 'PUT') { 108 | const url = new URL(request.url).pathname; 109 | await env.COMPRESSOR_BUCKET.put(url, request.body); 110 | await env.COMPRESSOR_WORKFLOW.create({ params: { r2Path: url, id: url } }); 111 | return new Response('ok'); 112 | } 113 | 114 | return new Response('hit with POST to compress anything, PUT to upload it to R2 and do the compression async'); 115 | }, 116 | } satisfies ExportedHandler; 117 | -------------------------------------------------------------------------------- /compression-workflows/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /compression-workflows/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "compressor-workflows", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "Compressor", 10 | ], 11 | "tag": "v1" 12 | } 13 | ], 14 | "containers": [{ 15 | "name": "compressor-workflows", 16 | "image": "./Dockerfile", 17 | "class_name": "Compressor", 18 | "max_instances": 2 19 | }], 20 | "durable_objects": { 21 | "bindings": [ 22 | { 23 | "class_name": "Compressor", 24 | "name": "COMPRESSOR" 25 | }, 26 | ] 27 | }, 28 | "observability": { 29 | "enabled": true 30 | }, 31 | "workflows": [ 32 | { 33 | "name": "compressor-workflow", 34 | "binding": "COMPRESSOR_WORKFLOW", 35 | "class_name": "CompressorWorkflow" 36 | } 37 | ], 38 | "r2_buckets": [ 39 | { 40 | "bucket_name": "compressor-results", 41 | "binding": "COMPRESSOR_BUCKET" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /compression/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /compression/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /compression/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /compression/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /compression/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM scratch 3 | COPY container/server /server 4 | EXPOSE 8080 5 | # Run 6 | CMD ["/server"] 7 | -------------------------------------------------------------------------------- /compression/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23 4 | 5 | require github.com/klauspost/compress v1.18.0 6 | -------------------------------------------------------------------------------- /compression/container/go.sum: -------------------------------------------------------------------------------- 1 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 2 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 3 | -------------------------------------------------------------------------------- /compression/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/klauspost/compress/zstd" 11 | ) 12 | 13 | var logs bytes.Buffer 14 | 15 | func main() { 16 | go func() { 17 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 18 | w.Write(logs.Bytes()) 19 | }) 20 | 21 | http.ListenAndServe(":8002", nil) 22 | }() 23 | 24 | ld, err := net.Listen("tcp", "0.0.0.0:8001") 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | for { 30 | conn, err := ld.Accept() 31 | if err != nil { 32 | panic(err) 33 | } 34 | writer, err := zstd.NewWriter(conn, zstd.WithEncoderConcurrency(1)) 35 | if err != nil { 36 | fmt.Fprintln(&logs, "error new writer:", err) 37 | conn.Close() 38 | return 39 | } 40 | 41 | n, err := io.Copy(writer, conn) 42 | if err != nil { 43 | fmt.Fprintln(&logs, "error new copy:", err) 44 | conn.Close() 45 | return 46 | } 47 | 48 | fmt.Fprintln(&logs, "Written", n) 49 | writer.Close() 50 | conn.Close() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /compression/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compression", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /compression/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from 'cloudflare:workers'; 2 | 3 | export class Compressor extends DurableObject { 4 | container: globalThis.Container; 5 | monitor?: Promise; 6 | 7 | constructor(ctx: DurableObjectState, env: Env) { 8 | super(ctx, env); 9 | if (ctx.container === undefined) throw new Error('no container'); 10 | this.container = ctx.container; 11 | ctx.blockConcurrencyWhile(async () => { 12 | if (!this.container.running) this.container.start({ entrypoint: ['/server'], enableInternet: false }); 13 | this.monitor = this.container.monitor().then(() => console.log('Container exited?')); 14 | }); 15 | } 16 | 17 | async init() { 18 | console.log('Starting container'); 19 | } 20 | 21 | async logs() { 22 | return await this.container.getTcpPort(8002).fetch('http://container'); 23 | } 24 | 25 | async compressString(value: string): Promise { 26 | const conn = this.container.getTcpPort(8001).connect('10.0.0.1:8001'); 27 | await conn.opened; 28 | 29 | const encoder = new TextEncoder(); 30 | const view = encoder.encode(value); 31 | const read = conn.readable; 32 | 33 | const writer = conn.writable.getWriter(); 34 | await writer.write(view).then(async () => { 35 | await writer.close(); 36 | }); 37 | 38 | return new Response(read); 39 | } 40 | 41 | async fetch(request: Request): Promise { 42 | const conn = this.container.getTcpPort(8001).connect('10.0.0.1:8001'); 43 | await conn.opened; 44 | 45 | const read = conn.readable; 46 | 47 | const writer = conn.writable; 48 | void request.body?.pipeTo(writer).then(() => { 49 | writer.close(); 50 | }); 51 | 52 | return new Response(read); 53 | } 54 | } 55 | 56 | export default { 57 | async fetch(request: Request, env: Env): Promise { 58 | const stub = env.COMPRESSOR.get(env.COMPRESSOR.idFromName('compressor')); 59 | if (request.method === 'POST') { 60 | try { 61 | const value = await request.text(); 62 | const bytes = await stub.compressString(value); 63 | return bytes; 64 | } catch (err) { 65 | return new Response(err.message, { status: 500 }); 66 | } 67 | } 68 | 69 | if (request.method === 'PUT') { 70 | return stub.fetch(request); 71 | } 72 | 73 | if (request.url.includes('logs')) { 74 | try { 75 | return await stub.logs(); 76 | } catch (err) { 77 | return new Response(err.message); 78 | } 79 | } 80 | 81 | await stub.init(); 82 | return new Response('hit with POST to compress anything'); 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /compression/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /compression/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "compressor", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "Compressor", 10 | ], 11 | "tag": "v1" 12 | } 13 | ], 14 | "containers": [{ 15 | "name": "compressor", 16 | "image": "./Dockerfile", 17 | "class_name": "Compressor", 18 | "max_instances": 5 19 | }], 20 | "durable_objects": { 21 | "bindings": [ 22 | { 23 | "class_name": "Compressor", 24 | "name": "COMPRESSOR" 25 | }, 26 | ] 27 | }, 28 | "observability": { 29 | "enabled": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /compute/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /compute/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.23 AS build 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY container_src/go.mod ./ 9 | RUN go mod download 10 | 11 | # Copy container src 12 | COPY container_src/*.go ./ 13 | # Build 14 | RUN CGO_ENABLED=0 GOOS=linux go build -o /server 15 | 16 | FROM scratch 17 | COPY --from=build /server /server 18 | EXPOSE 8080 19 | # Run 20 | ENTRYPOINT ["/server"] 21 | -------------------------------------------------------------------------------- /compute/README.md: -------------------------------------------------------------------------------- 1 | # Compute example 2 | 3 | This example shows how you could have multiple classes of compute managed by a single worker + a DO container manager that manages multiple DO container bindings. 4 | 5 | ## How to deploy 6 | 7 | To deploy this example all that is required is the appropriate permission and a call to `pnpm run deploy`. 8 | 9 | 10 | ## Launching a compute job 11 | 12 | To launch a compute job with this example you need to make a request like: 13 | 14 | ``` 15 | curl https:///start \ 16 | -x POST \ 17 | -d '{"name":"my-first-job", "envVars": {"VAR_ONE":"something", "VAR_TWO": "something else"}, "enableInternet": false, "size": "large", "entrypoint": ["/server"] }' 18 | ``` 19 | 20 | This will launch a compute job with the name `my-first-job` with the provided env variables, entrypoint, and compute size selected. 21 | 22 | If using the example docker image in the repo, you can check the jobs details by querying the container directly by doing: 23 | 24 | ``` 25 | curl https:///container/my-first-job 26 | ... 27 | Hi, I'm a container running in sin09, SG, which is part of APAC 28 | My env Vars are: 29 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 30 | CLOUDFLARE_APPLICATION_ID= 31 | CLOUDFLARE_COUNTRY_A2=SG 32 | CLOUDFLARE_DEPLOYMENT_ID= 33 | CLOUDFLARE_LOCATION=sin09 34 | CLOUDFLARE_REGION=APAC 35 | VAR_ONE=something 36 | VAR_TWO=something else 37 | CLOUDFLARE_DURABLE_OBJECT_ID= 38 | HOME=/ 39 | I was started with args: 40 | /server 41 | I have 3 cpus 42 | I am shutting down in 100 seconds 43 | ``` 44 | 45 | ## How does this work? 46 | 47 | ### Wrangler configuration 48 | 49 | Lets go through the wrangler.jsonc section by section. 50 | 51 | ```json 52 | "migrations": [ 53 | { 54 | "new_sqlite_classes": [ 55 | "MyContainerSmall", 56 | "MyContainerMedium", 57 | "MyContainerLarge", 58 | "MyContainerManager" 59 | ], 60 | "tag": "v1" 61 | } 62 | ], 63 | ``` 64 | 65 | Here we are defining our sqlite classes. We have 4, 3 container binding DO's and one manager DO. 66 | 67 | 68 | ```json 69 | "containers": [ 70 | { 71 | "name": "small", 72 | "image": "./Dockerfile", 73 | "max_instances": 5, 74 | "configuration" : { 75 | "vcpu": 1, 76 | "memory": "1GB" 77 | }, 78 | "class_name": "MyContainerSmall" 79 | 80 | }, 81 | { 82 | "name": "medium", 83 | "image": "./Dockerfile", 84 | "max_instances": 5, 85 | "configuration" : { 86 | "vcpu": 2, 87 | "memory": "2GB" 88 | }, 89 | "class_name": "MyContainerMedium" 90 | 91 | }, 92 | { 93 | "name": "large", 94 | "image": "./Dockerfile", 95 | "max_instances": 5, 96 | "configuration" : { 97 | "vcpu": 3, 98 | "memory": "3GB" 99 | }, 100 | "class_name": "MyContainerLarge" 101 | 102 | } 103 | ], 104 | 105 | ``` 106 | 107 | In our array of container bindings we have defined 3 compute sizes: small, medium, and large. 108 | The names are chosen arbitrarily and are used for targeting the correct binding at runtime. 109 | We use the same container image for all 3 but you could use a different image for each one by changing the image argument. 110 | 111 | ```json 112 | "durable_objects": { 113 | "bindings": [ 114 | { 115 | "class_name": "MyContainerSmall", 116 | "name": "CONTAINER_SMALL" 117 | }, 118 | { 119 | "class_name": "MyContainerMedium", 120 | "name": "CONTAINER_MEDIUM" 121 | }, 122 | { 123 | "class_name": "MyContainerLarge", 124 | "name": "CONTAINER_LARGE" 125 | }, 126 | { 127 | "class_name": "MyContainerManager", 128 | "name": "CONTAINER_MANAGER" 129 | } 130 | ] 131 | }, 132 | 133 | ``` 134 | 135 | Last we bind our DO classes to a binding name that we can reference inside the worker. 136 | 137 | ### Connecting with code 138 | 139 | Inside our base `CommonContainerManager` class we accept as part of the constructor a map of binding names to bindings. 140 | This allows us to lookup the correct binding to use by name at runtime. 141 | 142 | ```typescript 143 | export type ContainerBindingMap = Record; 144 | 145 | export class CommonContainerManager extends DurableObject { 146 | constructor( 147 | ctx: DurableObjectState, 148 | env: Env, 149 | bindings: ContainerBindingMap, 150 | ) { 151 | super(ctx, env); 152 | this.ctx = ctx; 153 | this.env = env; 154 | this.bindings = bindings; 155 | this.setAlarm(Date.now()); 156 | } 157 | ``` 158 | 159 | 160 | And in our `MyContainerManager` which extends this class we provide the bindingMap to the constructor mapping our 3 bindings to the names that reference them: 161 | 162 | ```typescript 163 | export class MyContainerManager extends CommonContainerManager { 164 | constructor(ctx: DurableObjectState, env: Env) { 165 | const bindingMap: ContainerBindingMap = { 166 | small: env.CONTAINER_SMALL, 167 | medium: env.CONTAINER_MEDIUM, 168 | large: env.CONTAINER_LARGE, 169 | }; 170 | super(ctx, env, bindingMap); 171 | this.ctx = ctx; 172 | this.env = env; 173 | } 174 | 175 | ``` 176 | 177 | 178 | This enables us to call target a specific compute size from our worker by doing: 179 | 180 | ```typescript 181 | await manager.newContainer( 182 | { 183 | env: startOpts.envVars, 184 | entrypoint: startOpts.entrypoint, 185 | enableInternet: startOpts.enableInternet, 186 | }, 187 | startOpts.name, 188 | startOpts.size, 189 | ); 190 | ``` 191 | 192 | Where startOpts.size is the users requested compute size. 193 | 194 | 195 | -------------------------------------------------------------------------------- /compute/common/container.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from "cloudflare:workers"; 2 | 3 | // starting => we called start() and init the monitor promise 4 | // running => container returned healthy on the endpoint 5 | // unhealthy => container is unhealthy (returning not OK status codes) 6 | // stopped => container is stopped (finished running) 7 | // failed => container failed to run and it won't try to run again, unless called 'start' again 8 | // 9 | // As written class Container will only use running and stopped. If you define a healthcheck() 10 | // function that returns ContainerState, it will call it in it's alarm and update the state to the result 11 | // of that call. 12 | export type ContainerState = 13 | | "starting" 14 | | "running" 15 | | "unhealthy" 16 | | "stopped" 17 | | "failed" 18 | | "unknown"; 19 | 20 | async function wrap( 21 | fn: Promise, 22 | ): Promise<[T, null] | [null, E]> { 23 | return fn 24 | .then((data) => [data, null] as [T, null]) 25 | .catch((err) => [null, err as unknown as E] as [null, E]); 26 | } 27 | 28 | function isNotListeningError(err: Error): boolean { 29 | return err.message.includes("the container is not listening"); 30 | } 31 | 32 | function noContainerYetError(err: Error): boolean { 33 | return err.message.includes("there is no container instance"); 34 | } 35 | 36 | function wait(ms: number): Promise { 37 | return new Promise((res) => setTimeout(res, ms)); 38 | } 39 | 40 | export class CommonContainer extends DurableObject { 41 | container: globalThis.Container; 42 | monitor?: Promise; 43 | 44 | constructor(ctx: DurableObjectState, env: Env) { 45 | if (ctx.container === undefined) { 46 | throw new Error("container is not defined"); 47 | } 48 | 49 | super(ctx, env); 50 | this.ctx = ctx; 51 | this.env = env; 52 | this.container = ctx.container; 53 | this.ctx.blockConcurrencyWhile(async () => { 54 | if (this.container.running) { 55 | if (this.monitor === undefined) { 56 | this.monitor = this.container.monitor(); 57 | this.handleMonitorPromise(this.monitor); 58 | } 59 | } 60 | 61 | // if no alarm, trigger ASAP 62 | await this.setAlarm(Date.now()); 63 | }); 64 | } 65 | async state(): Promise { 66 | const state = (await this.ctx.storage.get("state")) ?? "unknown"; 67 | return state; 68 | } 69 | 70 | async stateTx(cb: (state: ContainerState) => Promise) { 71 | return await this.ctx.blockConcurrencyWhile(async () => { 72 | const s = await this.state(); 73 | await cb(s); 74 | }); 75 | } 76 | 77 | public async preStateChange( 78 | current: ContainerState, 79 | newState: ContainerState, 80 | ) { 81 | return; 82 | } 83 | 84 | public async postStateChange(old: ContainerState, current: ContainerState) { 85 | // this is a no-op 86 | // implement it for your own behavior 87 | return; 88 | } 89 | 90 | private async setState(state: ContainerState) { 91 | console.log("Setting container state", state); 92 | const oldState = await this.ctx.storage.get("state"); 93 | await this.preStateChange(oldState, state); 94 | await this.ctx.storage.put("state", state); 95 | await this.ctx.storage.sync(); 96 | await this.postStateChange(oldState, state); 97 | } 98 | 99 | async setAlarm(value = Date.now() + 500) { 100 | const alarm = await this.ctx.storage.getAlarm(); 101 | if (alarm === null) { 102 | await this.ctx.storage.setAlarm(value); 103 | await this.ctx.storage.sync(); 104 | } 105 | } 106 | 107 | async alarm() { 108 | try { 109 | await this.stateTx(async (state) => { 110 | console.log("Current container state:", state); 111 | const maybeHealthCheck = (this as any).healthCheck; 112 | 113 | if (typeof maybeHealthcheck === "function") { 114 | const [result, err] = await wrap(maybeHealthCheck()); 115 | if (err !== null) { 116 | console.error( 117 | "Received an internal error from healthCheck:", 118 | err.message, 119 | ); 120 | if (state !== "starting") { 121 | await this.setState("failed"); 122 | } 123 | 124 | return; 125 | } 126 | 127 | if (typeof result !== "string") { 128 | console.warn( 129 | "Container is unhealthy because it returned a ", 130 | result.status, 131 | ); 132 | 133 | // consume text stream 134 | await wrap(result.text()); 135 | 136 | await this.setState("unhealthy"); 137 | return; 138 | } 139 | 140 | if (result === "ok") { 141 | await this.setState("running"); 142 | return; 143 | } 144 | 145 | if (result == "not_listening" || result == "no_container_yet") { 146 | await this.setState("starting"); 147 | return; 148 | } 149 | 150 | console.error("unknown result:", result); 151 | } else { 152 | await this.setState(state); 153 | console.log("No healthcheck function defined."); 154 | } 155 | }); 156 | } catch (error) { 157 | console.error("error during alarm: ", error, error.message); 158 | } finally { 159 | await this.setAlarm(); 160 | } 161 | } 162 | 163 | handleMonitorPromise(monitor: Promise) { 164 | monitor 165 | .then(async () => { 166 | await this.stateTx(async (state) => { 167 | if (state === "running" || state == "unhealthy") { 168 | await this.setState("stopped"); 169 | console.log(`Container stopped from state ${state}`); 170 | return; 171 | } 172 | 173 | if (state === "starting") { 174 | console.log( 175 | "Container was starting, and monitor resolved, we might have had an exception, retrying later", 176 | ); 177 | this.handleMonitorPromise(this.monitor); 178 | return; 179 | } 180 | 181 | if (state === "failed") { 182 | console.log( 183 | "Container was marked as failed, but we resolved monitor successfully", 184 | ); 185 | } 186 | }); 187 | }) 188 | .catch(async (err) => { 189 | console.error(`Monitor exited with an error: ${err.message}`); 190 | await this.setState("failed"); 191 | }); 192 | } 193 | 194 | // 'start' will start the container, and it will make sure it runs until the end 195 | async start(containerStart?: ContainerStartupOptions) { 196 | if (this.container.running) { 197 | if (this.monitor === undefined) { 198 | this.monitor = this.container.monitor(); 199 | this.handleMonitorPromise(this.monitor); 200 | } 201 | 202 | return; 203 | } 204 | 205 | await this.container.start(containerStart); 206 | await this.setState("running"); 207 | this.monitor = this.container.monitor(); 208 | this.handleMonitorPromise(this.monitor); 209 | } 210 | 211 | // This ALWAYS throws an exception because it resets the DO 212 | async destroy() { 213 | try { 214 | await this.ctx.storage.deleteAll(); 215 | await this.ctx.storage.deleteAlarm(); 216 | await this.container.destroy(); 217 | } finally { 218 | this.ctx.abort(); 219 | } 220 | } 221 | 222 | public async fetch(request: Request, port = 8080): Promise { 223 | return await this.ctx.container 224 | .getTcpPort(port) 225 | .fetch(request.url.replace("https://", "http://"), request); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /compute/common/manager.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from "cloudflare:workers"; 2 | import type { CommonContainer, ContainerState } from "./container"; 3 | 4 | export type ContainerInfo = { 5 | startupOpts: ContainerStartupOptions; 6 | name: string; 7 | state: ContainerState; 8 | bindingName: string; 9 | }; 10 | 11 | export type ContainerBindingMap = Record; 12 | 13 | export class CommonContainerManager extends DurableObject { 14 | constructor( 15 | ctx: DurableObjectState, 16 | env: Env, 17 | bindings: ContainerBindingMap, 18 | ) { 19 | super(ctx, env); 20 | this.ctx = ctx; 21 | this.env = env; 22 | this.bindings = bindings; 23 | this.setAlarm(Date.now()); 24 | } 25 | 26 | private async getBinding(name: string): DurableObjectNamespace { 27 | const keys = Object.keys(this.bindings); 28 | if (name === "") { 29 | if (keys.length === 0) { 30 | throw new Error("No container binding provided"); 31 | } else if (keys.length === 1) { 32 | return this.bindings[keys[0]]; 33 | } else { 34 | throw new Error( 35 | "Multiple container bindings present: you must specify a name.", 36 | ); 37 | } 38 | } else { 39 | const binding = this.bindings[name]; 40 | if (!binding) { 41 | throw new Error(`No container binding found for name: '${name}'`); 42 | } 43 | return binding; 44 | } 45 | } 46 | 47 | private async getContainerBinding( 48 | bindingName: string, 49 | name: string, 50 | ): DurableObjectStub { 51 | const binding = await this.getBinding(bindingName); 52 | const id = binding.idFromName(name); 53 | return await binding.get(id); 54 | } 55 | // launches a newContainer with the given properties 56 | public async newContainer( 57 | opts: ContainerStartupOptions, 58 | name: string, 59 | bindingName?: string, 60 | ): Promise { 61 | const container = await this.getContainerBinding(bindingName, name); 62 | 63 | container.start(opts); 64 | await this.addContainer(name, opts, bindingName); 65 | } 66 | 67 | // forwards a request to the named container 68 | public async requestContainer( 69 | name: string, 70 | request: Request, 71 | ): Promise { 72 | const containers = await this.ctx.storage.get("containers"); 73 | const info = containers.filter((c) => c.name === name); 74 | if (info.length !== 1) { 75 | throw new Error(`No record found for container: ${name}`); 76 | } 77 | 78 | const container = await this.getContainerBinding(info[0].bindingName, name); 79 | 80 | return await container.fetch(request); 81 | } 82 | 83 | // {add,rm}Container handle updating DO storage of it's containers 84 | public async addContainer( 85 | name: string, 86 | opts: ContainerStartupOptions, 87 | bindingName: string, 88 | ): Promise { 89 | let containers = await this.ctx.storage.get("containers"); 90 | if (!containers) { 91 | containers = []; 92 | } 93 | if (!containers.includes(name)) { 94 | containers.push({ 95 | name: name, 96 | startupOpts: opts, 97 | state: "starting", 98 | bindingName: bindingName, 99 | }); 100 | await this.ctx.storage.put("containers", containers); 101 | await this.ctx.storage.sync(); 102 | } 103 | } 104 | 105 | public async rmContainer(name: string): Promise { 106 | let containers = await this.ctx.storage.get("containers"); 107 | if (!containers || containers.length === 0) { 108 | return false; 109 | } 110 | const beforeLength = containers.length; 111 | containers = containers.filter((d) => d !== name); 112 | if (containers.length !== beforeLength) { 113 | await this.ctx.storage.put("containers", containers); 114 | return true; 115 | } 116 | return false; 117 | } 118 | 119 | // returns info on the containers this DO is managing 120 | public async listContainers(): Promise> { 121 | const list = await this.ctx.storage.get("containers"); 122 | if (!list) { 123 | return []; 124 | } 125 | return list; 126 | } 127 | 128 | public async updateContainerStates(): Promise { 129 | this.ctx.blockConcurrencyWhile(async () => { 130 | const containers = 131 | await this.ctx.storage.get("containers"); 132 | const updated: ContainerInfo[] = []; 133 | const removals: ContainerInfo[] = []; 134 | for (const container of containers) { 135 | const c = await this.getContainerBinding( 136 | container.bindingName, 137 | container.name, 138 | ); 139 | const state = await c.state(); 140 | 141 | if (this.isTerminalState(state)) { 142 | //decide if we should remove or not 143 | const remove = this.handleTerminalState(c); 144 | if (remove) { 145 | removals.push(container); 146 | continue; 147 | } 148 | } 149 | container.state = state; 150 | updated.push(container); 151 | } 152 | for (const rm in removals) { 153 | await this.rmContainer(rm.name); 154 | } 155 | await this.ctx.storage.put("containers", updated); 156 | await this.ctx.storage.sync(); 157 | }); 158 | } 159 | 160 | async setAlarm(value = Date.now() + 1000) { 161 | const alarm = await this.ctx.storage.getAlarm(); 162 | if (alarm === null) { 163 | await this.ctx.storage.setAlarm(value); 164 | await this.ctx.storage.sync(); 165 | } 166 | } 167 | 168 | async alarm() { 169 | try { 170 | await this.updateContainerStates(); 171 | } finally { 172 | await this.setAlarm(); 173 | } 174 | } 175 | 176 | isTerminalState(state: ContainerState): boolean { 177 | return state === "stopped" || state === "failed"; 178 | } 179 | 180 | // Default behavior destroys and removes container on terminal state 181 | async handleTerminalState(container: Container): boolean { 182 | container.destroy(); 183 | return true; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /compute/container_src/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23.2 4 | -------------------------------------------------------------------------------- /compute/container_src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | "time" 10 | ) 11 | 12 | var ttl = 0 13 | 14 | func handler(w http.ResponseWriter, r *http.Request) { 15 | country := os.Getenv("CLOUDFLARE_COUNTRY_A2") 16 | location := os.Getenv("CLOUDFLARE_LOCATION") 17 | region := os.Getenv("CLOUDFLARE_REGION") 18 | text := fmt.Sprintf("Hi, I'm a container running in %s, %s, which is part of %s\n", location, country, region) 19 | text += "My env Vars are: \n" 20 | allVars := os.Environ() 21 | for _, env := range allVars { 22 | text += env + "\n" 23 | } 24 | text += "I was started with args:\n" 25 | for _, arg := range os.Args { 26 | text += arg + "\n" 27 | } 28 | 29 | text += fmt.Sprintf("I have %d cpus \n", runtime.NumCPU()) 30 | text += fmt.Sprintf("I am shutting down in %d seconds\n", ttl) 31 | fmt.Fprintf(w, text) 32 | } 33 | 34 | func main() { 35 | http.HandleFunc("/", handler) 36 | http.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { 37 | w.Write([]byte("ok")) 38 | }) 39 | 40 | go func() { 41 | for i := 0; i < 120; i++ { 42 | time.Sleep(time.Second) 43 | ttl = 120 - i 44 | } 45 | os.Exit(0) 46 | }() 47 | log.Fatal(http.ListenAndServe(":8080", nil)) 48 | } 49 | -------------------------------------------------------------------------------- /compute/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compute", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /compute/src/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { CommonContainerManager } from "../common/manager"; 3 | import { CommonContainer } from "../common/container"; 4 | import type { ContainerState } from "../common/container"; 5 | import type { ContainerBindingMap } from "../common/manager"; 6 | 7 | export class MyContainer extends CommonContainer { 8 | // healthCheck returns 'ok' when the container returned 9 | // in the port returned a successful status code. 10 | // It will return a Response object when the status code is not ok. 11 | // It will return a known error enum if the container is not ready yet. 12 | public override async healthCheck( 13 | portNumber = 8080, 14 | ): Promise<"ok" | "not_listening" | "no_container_yet" | Response> { 15 | const port = this.container.getTcpPort(portNumber); 16 | const [res, err] = await wrap( 17 | port.fetch(new Request("http://container/_health")), 18 | ); 19 | if (err !== null) { 20 | if (isNotListeningError(err)) { 21 | return "not_listening"; 22 | } 23 | 24 | if (noContainerYetError(err)) { 25 | return "no_container_yet"; 26 | } 27 | 28 | throw err; 29 | } 30 | 31 | if (res.ok) { 32 | await res.text(); 33 | return "ok"; 34 | } 35 | 36 | // let the end user handle the not ok status code 37 | return res; 38 | } 39 | } 40 | 41 | // Here we define our 3 classes that simply extend a common base class. 42 | // This is just to give us a target class for the different compute classes we 43 | // defined in our wrangler.jsonc. 44 | // 45 | // We don't in this example but you could define different behavior for each one. 46 | export class MyContainerSmall extends MyContainer { 47 | constructor(ctx: DurableObjectState, env: Env) { 48 | super(ctx, env); 49 | this.ctx = ctx; 50 | this.env = env; 51 | } 52 | } 53 | 54 | export class MyContainerMedium extends MyContainer { 55 | constructor(ctx: DurableObjectState, env: Env) { 56 | super(ctx, env); 57 | this.ctx = ctx; 58 | this.env = env; 59 | } 60 | } 61 | 62 | export class MyContainerLarge extends MyContainer { 63 | constructor(ctx: DurableObjectState, env: Env) { 64 | super(ctx, env); 65 | this.ctx = ctx; 66 | this.env = env; 67 | } 68 | } 69 | 70 | export class MyContainerManager extends CommonContainerManager { 71 | constructor(ctx: DurableObjectState, env: Env) { 72 | // The bindingMap we pass into CommonContainerManagers constructor here 73 | // is what allows us to target a specific binding based on the users 74 | // requested size. 75 | const bindingMap: ContainerBindingMap = { 76 | small: env.CONTAINER_SMALL, 77 | medium: env.CONTAINER_MEDIUM, 78 | large: env.CONTAINER_LARGE, 79 | }; 80 | super(ctx, env, bindingMap); 81 | this.ctx = ctx; 82 | this.env = env; 83 | } 84 | 85 | public override async handleTerminalState( 86 | container: CommonContainer, 87 | ): boolean { 88 | if (c instanceof MyContainer) { 89 | // I never want my containers to be restarted 90 | return true; 91 | } else { 92 | return super.handleTerminalState(container); 93 | } 94 | } 95 | } 96 | 97 | // Define a schema for our /start request 98 | const startSchema = z.object({ 99 | name: z.string(), 100 | envVars: z.record(z.string(), z.string()).optional(), 101 | entrypoint: z.array(z.string()).optional(), 102 | enableInternet: z.boolean().optional(), 103 | size: z.string(), 104 | }); 105 | 106 | export default { 107 | async fetch(request, env, ctx): Promise { 108 | // Grab our single DO Container manager 109 | const mid = env.CONTAINER_MANAGER.idFromName("manager"); 110 | const manager = env.CONTAINER_MANAGER.get(mid); 111 | const url = new URL(request.url); 112 | const pathname = url.pathname; 113 | 114 | // Assumes requests come with the pattern /container/ 115 | // where matches the name the user supplied to /start 116 | if (pathname.startsWith("/container/")) { 117 | const parts = pathname.split("/"); 118 | const lastPart = parts.pop(); 119 | return await manager.requestContainer(lastPart, request); 120 | } 121 | 122 | // Requst our manager to start a new container with the user-supplied options. 123 | if (pathname.startsWith("/start")) { 124 | try { 125 | const raw = await request.text(); 126 | const json = JSON.parse(raw); 127 | const startOpts = startSchema.parse(json); 128 | await manager.newContainer( 129 | { 130 | env: startOpts.envVars, 131 | entrypoint: startOpts.entrypoint, 132 | enableInternet: startOpts.enableInternet, 133 | }, 134 | startOpts.name, 135 | startOpts.size, 136 | ); 137 | return new Response("Container starting"); 138 | } catch (error) { 139 | console.error("error starting container: ", error); 140 | return new Response( 141 | JSON.stringify({ 142 | message: "Error parsing request", 143 | status: 500, 144 | statusText: error.message, 145 | headers: { 146 | "Content-Type": "application/json", 147 | }, 148 | }), 149 | ); 150 | } 151 | } 152 | 153 | // List the containers that are currently running. 154 | if (pathname.startsWith("/list")) { 155 | const runningContainers = await manager.listContainers(); 156 | return new Response(JSON.stringify(runningContainers)); 157 | } 158 | 159 | return new Response("Hello pool world", { 160 | headers: { "Content-Type": "text/html;charset=UTF-8" }, 161 | }); 162 | }, 163 | } satisfies ExportedHandler; 164 | -------------------------------------------------------------------------------- /compute/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | }, 44 | "include":[ 45 | "src", 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /compute/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "compute-example", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "MyContainerSmall", 10 | "MyContainerMedium", 11 | "MyContainerLarge", 12 | "MyContainerManager" 13 | ], 14 | "tag": "v1" 15 | } 16 | ], 17 | "containers": [ 18 | { 19 | "name": "small", 20 | "image": "./Dockerfile", 21 | "max_instances": 5, 22 | "configuration" : { 23 | "vcpu": 1, 24 | "memory": "1GB" 25 | }, 26 | "class_name": "MyContainerSmall" 27 | 28 | }, 29 | { 30 | "name": "medium", 31 | "image": "./Dockerfile", 32 | "max_instances": 5, 33 | "configuration" : { 34 | "vcpu": 2, 35 | "memory": "2GB" 36 | }, 37 | "class_name": "MyContainerMedium" 38 | 39 | }, 40 | { 41 | "name": "large", 42 | "image": "./Dockerfile", 43 | "max_instances": 5, 44 | "configuration" : { 45 | "vcpu": 3, 46 | "memory": "3GB" 47 | }, 48 | "class_name": "MyContainerLarge" 49 | 50 | } 51 | ], 52 | "durable_objects": { 53 | "bindings": [ 54 | { 55 | "class_name": "MyContainerSmall", 56 | "name": "CONTAINER_SMALL" 57 | }, 58 | { 59 | "class_name": "MyContainerMedium", 60 | "name": "CONTAINER_MEDIUM" 61 | }, 62 | { 63 | "class_name": "MyContainerLarge", 64 | "name": "CONTAINER_LARGE" 65 | }, 66 | { 67 | "class_name": "MyContainerManager", 68 | "name": "CONTAINER_MANAGER" 69 | } 70 | ] 71 | }, 72 | "observability": { 73 | "enabled": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /http2/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /http2/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /http2/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /http2/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /http2/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.24 AS build 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY container/go.mod container/go.sum ./ 9 | RUN go mod download 10 | 11 | # Copy container src 12 | COPY container/*.go ./ 13 | # Build 14 | RUN CGO_ENABLED=0 GOOS=linux go build -o /server 15 | 16 | FROM debian:latest 17 | COPY --from=build /server /server 18 | EXPOSE 8080 19 | # Run 20 | CMD ["/server"] 21 | -------------------------------------------------------------------------------- /http2/README.md: -------------------------------------------------------------------------------- 1 | # HTTP fetch to a container 2 | 3 | This example shows a simple HTTP request passthrough the DO to the container. 4 | -------------------------------------------------------------------------------- /http2/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.24 4 | 5 | require ( 6 | golang.org/x/net v0.39.0 // indirect 7 | golang.org/x/text v0.24.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /http2/container/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 2 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 3 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 4 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 5 | -------------------------------------------------------------------------------- /http2/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "golang.org/x/net/http2" 13 | "golang.org/x/net/http2/h2c" 14 | ) 15 | 16 | func handler(w http.ResponseWriter, r *http.Request) { 17 | country := os.Getenv("CLOUDFLARE_COUNTRY_A2") 18 | location := os.Getenv("CLOUDFLARE_LOCATION") 19 | region := os.Getenv("CLOUDFLARE_REGION") 20 | 21 | fmt.Fprintf(w, "Hi, I'm a container running in %s, %s, which is part of %s ", location, country, region) 22 | } 23 | 24 | func main() { 25 | c := make(chan os.Signal, 10) 26 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 27 | terminate := false 28 | go func() { 29 | for range c { 30 | if terminate { 31 | os.Exit(0) 32 | continue 33 | } 34 | 35 | terminate = true 36 | go func() { 37 | time.Sleep(time.Minute) 38 | os.Exit(0) 39 | }() 40 | } 41 | }() 42 | 43 | mux := http.NewServeMux() 44 | mux.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { 45 | if terminate { 46 | w.WriteHeader(400) 47 | w.Write([]byte("draining")) 48 | return 49 | } 50 | 51 | w.Write([]byte("ok")) 52 | }) 53 | 54 | mux.HandleFunc("/protocol", func(w http.ResponseWriter, r *http.Request) { 55 | fmt.Fprintf(w, "Request Protocol: %s\n", r.Proto) 56 | }) 57 | 58 | mux.HandleFunc("/", handler) 59 | 60 | server := &http.Server{ 61 | Addr: "0.0.0.0:8080", 62 | Handler: h2c.NewHandler(mux, &http2.Server{}), 63 | } 64 | 65 | log.Fatal(server.ListenAndServe()) 66 | } 67 | -------------------------------------------------------------------------------- /http2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http2", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /http2/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@cloudflare/workers-types': 12 | specifier: ^4.20250403.0 13 | version: 4.20250409.0 14 | typescript: 15 | specifier: ^5.5.2 16 | version: 5.8.3 17 | wrangler: 18 | specifier: ^4.14.4 19 | version: 4.14.4(@cloudflare/workers-types@4.20250409.0) 20 | 21 | packages: 22 | 23 | '@cloudflare/kv-asset-handler@0.4.0': 24 | resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} 25 | engines: {node: '>=18.0.0'} 26 | 27 | '@cloudflare/unenv-preset@2.3.1': 28 | resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==} 29 | peerDependencies: 30 | unenv: 2.0.0-rc.15 31 | workerd: ^1.20250320.0 32 | peerDependenciesMeta: 33 | workerd: 34 | optional: true 35 | 36 | '@cloudflare/workerd-darwin-64@1.20250507.0': 37 | resolution: {integrity: sha512-xC+8hmQuOUUNCVT9DWpLMfxhR4Xs4kI8v7Bkybh4pzGC85moH6fMfCBNaP0YQCNAA/BR56aL/AwfvMVGskTK/A==} 38 | engines: {node: '>=16'} 39 | cpu: [x64] 40 | os: [darwin] 41 | 42 | '@cloudflare/workerd-darwin-arm64@1.20250507.0': 43 | resolution: {integrity: sha512-Oynff5H8yM4trfUFaKdkOvPV3jac8mg7QC19ILZluCVgLx/JGEVLEJ7do1Na9rLqV8CK4gmUXPrUMX7uerhQgg==} 44 | engines: {node: '>=16'} 45 | cpu: [arm64] 46 | os: [darwin] 47 | 48 | '@cloudflare/workerd-linux-64@1.20250507.0': 49 | resolution: {integrity: sha512-/HAA+Zg/R7Q/Smyl835FUFKjotZN1UzN9j/BHBd0xKmKov97QkXAX8gsyGnyKqRReIOinp8x/8+UebTICR7VJw==} 50 | engines: {node: '>=16'} 51 | cpu: [x64] 52 | os: [linux] 53 | 54 | '@cloudflare/workerd-linux-arm64@1.20250507.0': 55 | resolution: {integrity: sha512-NMPibSdOYeycU0IrKkgOESFJQy7dEpHvuatZxQxlT+mIQK0INzI3irp2kKxhF99s25kPC4p+xg9bU3ugTrs3VQ==} 56 | engines: {node: '>=16'} 57 | cpu: [arm64] 58 | os: [linux] 59 | 60 | '@cloudflare/workerd-windows-64@1.20250507.0': 61 | resolution: {integrity: sha512-c91fhNP8ufycdIDqjVyKTqeb4ewkbAYXFQbLreMVgh4LLQQPDDEte8wCdmaFy5bIL0M9d85PpdCq51RCzq/FaQ==} 62 | engines: {node: '>=16'} 63 | cpu: [x64] 64 | os: [win32] 65 | 66 | '@cloudflare/workers-types@4.20250409.0': 67 | resolution: {integrity: sha512-yPxxwE5nr168huEfLNOB6904OsvIWcq0tWT23NMD6jT5SIp2ds3oOGANw7wz39r5y3jZYC2h1OnGwnZXJDDCOg==} 68 | 69 | '@cspotcode/source-map-support@0.8.1': 70 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 71 | engines: {node: '>=12'} 72 | 73 | '@emnapi/runtime@1.4.0': 74 | resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} 75 | 76 | '@esbuild/aix-ppc64@0.25.4': 77 | resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} 78 | engines: {node: '>=18'} 79 | cpu: [ppc64] 80 | os: [aix] 81 | 82 | '@esbuild/android-arm64@0.25.4': 83 | resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} 84 | engines: {node: '>=18'} 85 | cpu: [arm64] 86 | os: [android] 87 | 88 | '@esbuild/android-arm@0.25.4': 89 | resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} 90 | engines: {node: '>=18'} 91 | cpu: [arm] 92 | os: [android] 93 | 94 | '@esbuild/android-x64@0.25.4': 95 | resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} 96 | engines: {node: '>=18'} 97 | cpu: [x64] 98 | os: [android] 99 | 100 | '@esbuild/darwin-arm64@0.25.4': 101 | resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} 102 | engines: {node: '>=18'} 103 | cpu: [arm64] 104 | os: [darwin] 105 | 106 | '@esbuild/darwin-x64@0.25.4': 107 | resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} 108 | engines: {node: '>=18'} 109 | cpu: [x64] 110 | os: [darwin] 111 | 112 | '@esbuild/freebsd-arm64@0.25.4': 113 | resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} 114 | engines: {node: '>=18'} 115 | cpu: [arm64] 116 | os: [freebsd] 117 | 118 | '@esbuild/freebsd-x64@0.25.4': 119 | resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} 120 | engines: {node: '>=18'} 121 | cpu: [x64] 122 | os: [freebsd] 123 | 124 | '@esbuild/linux-arm64@0.25.4': 125 | resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} 126 | engines: {node: '>=18'} 127 | cpu: [arm64] 128 | os: [linux] 129 | 130 | '@esbuild/linux-arm@0.25.4': 131 | resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} 132 | engines: {node: '>=18'} 133 | cpu: [arm] 134 | os: [linux] 135 | 136 | '@esbuild/linux-ia32@0.25.4': 137 | resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} 138 | engines: {node: '>=18'} 139 | cpu: [ia32] 140 | os: [linux] 141 | 142 | '@esbuild/linux-loong64@0.25.4': 143 | resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} 144 | engines: {node: '>=18'} 145 | cpu: [loong64] 146 | os: [linux] 147 | 148 | '@esbuild/linux-mips64el@0.25.4': 149 | resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} 150 | engines: {node: '>=18'} 151 | cpu: [mips64el] 152 | os: [linux] 153 | 154 | '@esbuild/linux-ppc64@0.25.4': 155 | resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} 156 | engines: {node: '>=18'} 157 | cpu: [ppc64] 158 | os: [linux] 159 | 160 | '@esbuild/linux-riscv64@0.25.4': 161 | resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} 162 | engines: {node: '>=18'} 163 | cpu: [riscv64] 164 | os: [linux] 165 | 166 | '@esbuild/linux-s390x@0.25.4': 167 | resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} 168 | engines: {node: '>=18'} 169 | cpu: [s390x] 170 | os: [linux] 171 | 172 | '@esbuild/linux-x64@0.25.4': 173 | resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} 174 | engines: {node: '>=18'} 175 | cpu: [x64] 176 | os: [linux] 177 | 178 | '@esbuild/netbsd-arm64@0.25.4': 179 | resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} 180 | engines: {node: '>=18'} 181 | cpu: [arm64] 182 | os: [netbsd] 183 | 184 | '@esbuild/netbsd-x64@0.25.4': 185 | resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} 186 | engines: {node: '>=18'} 187 | cpu: [x64] 188 | os: [netbsd] 189 | 190 | '@esbuild/openbsd-arm64@0.25.4': 191 | resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} 192 | engines: {node: '>=18'} 193 | cpu: [arm64] 194 | os: [openbsd] 195 | 196 | '@esbuild/openbsd-x64@0.25.4': 197 | resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} 198 | engines: {node: '>=18'} 199 | cpu: [x64] 200 | os: [openbsd] 201 | 202 | '@esbuild/sunos-x64@0.25.4': 203 | resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} 204 | engines: {node: '>=18'} 205 | cpu: [x64] 206 | os: [sunos] 207 | 208 | '@esbuild/win32-arm64@0.25.4': 209 | resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} 210 | engines: {node: '>=18'} 211 | cpu: [arm64] 212 | os: [win32] 213 | 214 | '@esbuild/win32-ia32@0.25.4': 215 | resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} 216 | engines: {node: '>=18'} 217 | cpu: [ia32] 218 | os: [win32] 219 | 220 | '@esbuild/win32-x64@0.25.4': 221 | resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} 222 | engines: {node: '>=18'} 223 | cpu: [x64] 224 | os: [win32] 225 | 226 | '@fastify/busboy@2.1.1': 227 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 228 | engines: {node: '>=14'} 229 | 230 | '@img/sharp-darwin-arm64@0.33.5': 231 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 232 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 233 | cpu: [arm64] 234 | os: [darwin] 235 | 236 | '@img/sharp-darwin-x64@0.33.5': 237 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 238 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 239 | cpu: [x64] 240 | os: [darwin] 241 | 242 | '@img/sharp-libvips-darwin-arm64@1.0.4': 243 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 244 | cpu: [arm64] 245 | os: [darwin] 246 | 247 | '@img/sharp-libvips-darwin-x64@1.0.4': 248 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 249 | cpu: [x64] 250 | os: [darwin] 251 | 252 | '@img/sharp-libvips-linux-arm64@1.0.4': 253 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 254 | cpu: [arm64] 255 | os: [linux] 256 | 257 | '@img/sharp-libvips-linux-arm@1.0.5': 258 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 259 | cpu: [arm] 260 | os: [linux] 261 | 262 | '@img/sharp-libvips-linux-s390x@1.0.4': 263 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 264 | cpu: [s390x] 265 | os: [linux] 266 | 267 | '@img/sharp-libvips-linux-x64@1.0.4': 268 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 269 | cpu: [x64] 270 | os: [linux] 271 | 272 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 273 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 274 | cpu: [arm64] 275 | os: [linux] 276 | 277 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 278 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 279 | cpu: [x64] 280 | os: [linux] 281 | 282 | '@img/sharp-linux-arm64@0.33.5': 283 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 284 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 285 | cpu: [arm64] 286 | os: [linux] 287 | 288 | '@img/sharp-linux-arm@0.33.5': 289 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 290 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 291 | cpu: [arm] 292 | os: [linux] 293 | 294 | '@img/sharp-linux-s390x@0.33.5': 295 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 296 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 297 | cpu: [s390x] 298 | os: [linux] 299 | 300 | '@img/sharp-linux-x64@0.33.5': 301 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 302 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 303 | cpu: [x64] 304 | os: [linux] 305 | 306 | '@img/sharp-linuxmusl-arm64@0.33.5': 307 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 308 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 309 | cpu: [arm64] 310 | os: [linux] 311 | 312 | '@img/sharp-linuxmusl-x64@0.33.5': 313 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 314 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 315 | cpu: [x64] 316 | os: [linux] 317 | 318 | '@img/sharp-wasm32@0.33.5': 319 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 320 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 321 | cpu: [wasm32] 322 | 323 | '@img/sharp-win32-ia32@0.33.5': 324 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 325 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 326 | cpu: [ia32] 327 | os: [win32] 328 | 329 | '@img/sharp-win32-x64@0.33.5': 330 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 331 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 332 | cpu: [x64] 333 | os: [win32] 334 | 335 | '@jridgewell/resolve-uri@3.1.2': 336 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 337 | engines: {node: '>=6.0.0'} 338 | 339 | '@jridgewell/sourcemap-codec@1.5.0': 340 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 341 | 342 | '@jridgewell/trace-mapping@0.3.9': 343 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 344 | 345 | acorn-walk@8.3.2: 346 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 347 | engines: {node: '>=0.4.0'} 348 | 349 | acorn@8.14.0: 350 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 351 | engines: {node: '>=0.4.0'} 352 | hasBin: true 353 | 354 | as-table@1.0.55: 355 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 356 | 357 | blake3-wasm@2.1.5: 358 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 359 | 360 | color-convert@2.0.1: 361 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 362 | engines: {node: '>=7.0.0'} 363 | 364 | color-name@1.1.4: 365 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 366 | 367 | color-string@1.9.1: 368 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 369 | 370 | color@4.2.3: 371 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 372 | engines: {node: '>=12.5.0'} 373 | 374 | cookie@0.7.2: 375 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 376 | engines: {node: '>= 0.6'} 377 | 378 | data-uri-to-buffer@2.0.2: 379 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 380 | 381 | defu@6.1.4: 382 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 383 | 384 | detect-libc@2.0.3: 385 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 386 | engines: {node: '>=8'} 387 | 388 | esbuild@0.25.4: 389 | resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} 390 | engines: {node: '>=18'} 391 | hasBin: true 392 | 393 | exit-hook@2.2.1: 394 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 395 | engines: {node: '>=6'} 396 | 397 | exsolve@1.0.4: 398 | resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} 399 | 400 | fsevents@2.3.3: 401 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 402 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 403 | os: [darwin] 404 | 405 | get-source@2.0.12: 406 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 407 | 408 | glob-to-regexp@0.4.1: 409 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 410 | 411 | is-arrayish@0.3.2: 412 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 413 | 414 | mime@3.0.0: 415 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 416 | engines: {node: '>=10.0.0'} 417 | hasBin: true 418 | 419 | miniflare@4.20250507.0: 420 | resolution: {integrity: sha512-EgbQRt/Hnr8HCmW2J/4LRNE3yOzJTdNd98XJ8gnGXFKcimXxUFPiWP3k1df+ZPCtEHp6cXxi8+jP7v9vuIbIsg==} 421 | engines: {node: '>=18.0.0'} 422 | hasBin: true 423 | 424 | mustache@4.2.0: 425 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 426 | hasBin: true 427 | 428 | ohash@2.0.11: 429 | resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} 430 | 431 | path-to-regexp@6.3.0: 432 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 433 | 434 | pathe@2.0.3: 435 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 436 | 437 | printable-characters@1.0.42: 438 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 439 | 440 | semver@7.7.1: 441 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 442 | engines: {node: '>=10'} 443 | hasBin: true 444 | 445 | sharp@0.33.5: 446 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 447 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 448 | 449 | simple-swizzle@0.2.2: 450 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 451 | 452 | source-map@0.6.1: 453 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 454 | engines: {node: '>=0.10.0'} 455 | 456 | stacktracey@2.1.8: 457 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 458 | 459 | stoppable@1.1.0: 460 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 461 | engines: {node: '>=4', npm: '>=6'} 462 | 463 | tslib@2.8.1: 464 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 465 | 466 | typescript@5.8.3: 467 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 468 | engines: {node: '>=14.17'} 469 | hasBin: true 470 | 471 | ufo@1.6.1: 472 | resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 473 | 474 | undici@5.29.0: 475 | resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} 476 | engines: {node: '>=14.0'} 477 | 478 | unenv@2.0.0-rc.15: 479 | resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} 480 | 481 | workerd@1.20250507.0: 482 | resolution: {integrity: sha512-OXaGjEh5THT9iblwWIyPrYBoaPe/d4zN03Go7/w8CmS8sma7//O9hjbk43sboWkc89taGPmU0/LNyZUUiUlHeQ==} 483 | engines: {node: '>=16'} 484 | hasBin: true 485 | 486 | wrangler@4.14.4: 487 | resolution: {integrity: sha512-HIdOdiMIcJV5ymw80RKsr3Uzen/p1kRX4jnCEmR2XVeoEhV2Qw6GABxS5WMTlSES2/vEX0Y+ezUAdsprcUhJ5g==} 488 | engines: {node: '>=18.0.0'} 489 | hasBin: true 490 | peerDependencies: 491 | '@cloudflare/workers-types': ^4.20250507.0 492 | peerDependenciesMeta: 493 | '@cloudflare/workers-types': 494 | optional: true 495 | 496 | ws@8.18.0: 497 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 498 | engines: {node: '>=10.0.0'} 499 | peerDependencies: 500 | bufferutil: ^4.0.1 501 | utf-8-validate: '>=5.0.2' 502 | peerDependenciesMeta: 503 | bufferutil: 504 | optional: true 505 | utf-8-validate: 506 | optional: true 507 | 508 | youch@3.3.4: 509 | resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 510 | 511 | zod@3.22.3: 512 | resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 513 | 514 | snapshots: 515 | 516 | '@cloudflare/kv-asset-handler@0.4.0': 517 | dependencies: 518 | mime: 3.0.0 519 | 520 | '@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250507.0)': 521 | dependencies: 522 | unenv: 2.0.0-rc.15 523 | optionalDependencies: 524 | workerd: 1.20250507.0 525 | 526 | '@cloudflare/workerd-darwin-64@1.20250507.0': 527 | optional: true 528 | 529 | '@cloudflare/workerd-darwin-arm64@1.20250507.0': 530 | optional: true 531 | 532 | '@cloudflare/workerd-linux-64@1.20250507.0': 533 | optional: true 534 | 535 | '@cloudflare/workerd-linux-arm64@1.20250507.0': 536 | optional: true 537 | 538 | '@cloudflare/workerd-windows-64@1.20250507.0': 539 | optional: true 540 | 541 | '@cloudflare/workers-types@4.20250409.0': {} 542 | 543 | '@cspotcode/source-map-support@0.8.1': 544 | dependencies: 545 | '@jridgewell/trace-mapping': 0.3.9 546 | 547 | '@emnapi/runtime@1.4.0': 548 | dependencies: 549 | tslib: 2.8.1 550 | optional: true 551 | 552 | '@esbuild/aix-ppc64@0.25.4': 553 | optional: true 554 | 555 | '@esbuild/android-arm64@0.25.4': 556 | optional: true 557 | 558 | '@esbuild/android-arm@0.25.4': 559 | optional: true 560 | 561 | '@esbuild/android-x64@0.25.4': 562 | optional: true 563 | 564 | '@esbuild/darwin-arm64@0.25.4': 565 | optional: true 566 | 567 | '@esbuild/darwin-x64@0.25.4': 568 | optional: true 569 | 570 | '@esbuild/freebsd-arm64@0.25.4': 571 | optional: true 572 | 573 | '@esbuild/freebsd-x64@0.25.4': 574 | optional: true 575 | 576 | '@esbuild/linux-arm64@0.25.4': 577 | optional: true 578 | 579 | '@esbuild/linux-arm@0.25.4': 580 | optional: true 581 | 582 | '@esbuild/linux-ia32@0.25.4': 583 | optional: true 584 | 585 | '@esbuild/linux-loong64@0.25.4': 586 | optional: true 587 | 588 | '@esbuild/linux-mips64el@0.25.4': 589 | optional: true 590 | 591 | '@esbuild/linux-ppc64@0.25.4': 592 | optional: true 593 | 594 | '@esbuild/linux-riscv64@0.25.4': 595 | optional: true 596 | 597 | '@esbuild/linux-s390x@0.25.4': 598 | optional: true 599 | 600 | '@esbuild/linux-x64@0.25.4': 601 | optional: true 602 | 603 | '@esbuild/netbsd-arm64@0.25.4': 604 | optional: true 605 | 606 | '@esbuild/netbsd-x64@0.25.4': 607 | optional: true 608 | 609 | '@esbuild/openbsd-arm64@0.25.4': 610 | optional: true 611 | 612 | '@esbuild/openbsd-x64@0.25.4': 613 | optional: true 614 | 615 | '@esbuild/sunos-x64@0.25.4': 616 | optional: true 617 | 618 | '@esbuild/win32-arm64@0.25.4': 619 | optional: true 620 | 621 | '@esbuild/win32-ia32@0.25.4': 622 | optional: true 623 | 624 | '@esbuild/win32-x64@0.25.4': 625 | optional: true 626 | 627 | '@fastify/busboy@2.1.1': {} 628 | 629 | '@img/sharp-darwin-arm64@0.33.5': 630 | optionalDependencies: 631 | '@img/sharp-libvips-darwin-arm64': 1.0.4 632 | optional: true 633 | 634 | '@img/sharp-darwin-x64@0.33.5': 635 | optionalDependencies: 636 | '@img/sharp-libvips-darwin-x64': 1.0.4 637 | optional: true 638 | 639 | '@img/sharp-libvips-darwin-arm64@1.0.4': 640 | optional: true 641 | 642 | '@img/sharp-libvips-darwin-x64@1.0.4': 643 | optional: true 644 | 645 | '@img/sharp-libvips-linux-arm64@1.0.4': 646 | optional: true 647 | 648 | '@img/sharp-libvips-linux-arm@1.0.5': 649 | optional: true 650 | 651 | '@img/sharp-libvips-linux-s390x@1.0.4': 652 | optional: true 653 | 654 | '@img/sharp-libvips-linux-x64@1.0.4': 655 | optional: true 656 | 657 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 658 | optional: true 659 | 660 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 661 | optional: true 662 | 663 | '@img/sharp-linux-arm64@0.33.5': 664 | optionalDependencies: 665 | '@img/sharp-libvips-linux-arm64': 1.0.4 666 | optional: true 667 | 668 | '@img/sharp-linux-arm@0.33.5': 669 | optionalDependencies: 670 | '@img/sharp-libvips-linux-arm': 1.0.5 671 | optional: true 672 | 673 | '@img/sharp-linux-s390x@0.33.5': 674 | optionalDependencies: 675 | '@img/sharp-libvips-linux-s390x': 1.0.4 676 | optional: true 677 | 678 | '@img/sharp-linux-x64@0.33.5': 679 | optionalDependencies: 680 | '@img/sharp-libvips-linux-x64': 1.0.4 681 | optional: true 682 | 683 | '@img/sharp-linuxmusl-arm64@0.33.5': 684 | optionalDependencies: 685 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 686 | optional: true 687 | 688 | '@img/sharp-linuxmusl-x64@0.33.5': 689 | optionalDependencies: 690 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 691 | optional: true 692 | 693 | '@img/sharp-wasm32@0.33.5': 694 | dependencies: 695 | '@emnapi/runtime': 1.4.0 696 | optional: true 697 | 698 | '@img/sharp-win32-ia32@0.33.5': 699 | optional: true 700 | 701 | '@img/sharp-win32-x64@0.33.5': 702 | optional: true 703 | 704 | '@jridgewell/resolve-uri@3.1.2': {} 705 | 706 | '@jridgewell/sourcemap-codec@1.5.0': {} 707 | 708 | '@jridgewell/trace-mapping@0.3.9': 709 | dependencies: 710 | '@jridgewell/resolve-uri': 3.1.2 711 | '@jridgewell/sourcemap-codec': 1.5.0 712 | 713 | acorn-walk@8.3.2: {} 714 | 715 | acorn@8.14.0: {} 716 | 717 | as-table@1.0.55: 718 | dependencies: 719 | printable-characters: 1.0.42 720 | 721 | blake3-wasm@2.1.5: {} 722 | 723 | color-convert@2.0.1: 724 | dependencies: 725 | color-name: 1.1.4 726 | optional: true 727 | 728 | color-name@1.1.4: 729 | optional: true 730 | 731 | color-string@1.9.1: 732 | dependencies: 733 | color-name: 1.1.4 734 | simple-swizzle: 0.2.2 735 | optional: true 736 | 737 | color@4.2.3: 738 | dependencies: 739 | color-convert: 2.0.1 740 | color-string: 1.9.1 741 | optional: true 742 | 743 | cookie@0.7.2: {} 744 | 745 | data-uri-to-buffer@2.0.2: {} 746 | 747 | defu@6.1.4: {} 748 | 749 | detect-libc@2.0.3: 750 | optional: true 751 | 752 | esbuild@0.25.4: 753 | optionalDependencies: 754 | '@esbuild/aix-ppc64': 0.25.4 755 | '@esbuild/android-arm': 0.25.4 756 | '@esbuild/android-arm64': 0.25.4 757 | '@esbuild/android-x64': 0.25.4 758 | '@esbuild/darwin-arm64': 0.25.4 759 | '@esbuild/darwin-x64': 0.25.4 760 | '@esbuild/freebsd-arm64': 0.25.4 761 | '@esbuild/freebsd-x64': 0.25.4 762 | '@esbuild/linux-arm': 0.25.4 763 | '@esbuild/linux-arm64': 0.25.4 764 | '@esbuild/linux-ia32': 0.25.4 765 | '@esbuild/linux-loong64': 0.25.4 766 | '@esbuild/linux-mips64el': 0.25.4 767 | '@esbuild/linux-ppc64': 0.25.4 768 | '@esbuild/linux-riscv64': 0.25.4 769 | '@esbuild/linux-s390x': 0.25.4 770 | '@esbuild/linux-x64': 0.25.4 771 | '@esbuild/netbsd-arm64': 0.25.4 772 | '@esbuild/netbsd-x64': 0.25.4 773 | '@esbuild/openbsd-arm64': 0.25.4 774 | '@esbuild/openbsd-x64': 0.25.4 775 | '@esbuild/sunos-x64': 0.25.4 776 | '@esbuild/win32-arm64': 0.25.4 777 | '@esbuild/win32-ia32': 0.25.4 778 | '@esbuild/win32-x64': 0.25.4 779 | 780 | exit-hook@2.2.1: {} 781 | 782 | exsolve@1.0.4: {} 783 | 784 | fsevents@2.3.3: 785 | optional: true 786 | 787 | get-source@2.0.12: 788 | dependencies: 789 | data-uri-to-buffer: 2.0.2 790 | source-map: 0.6.1 791 | 792 | glob-to-regexp@0.4.1: {} 793 | 794 | is-arrayish@0.3.2: 795 | optional: true 796 | 797 | mime@3.0.0: {} 798 | 799 | miniflare@4.20250507.0: 800 | dependencies: 801 | '@cspotcode/source-map-support': 0.8.1 802 | acorn: 8.14.0 803 | acorn-walk: 8.3.2 804 | exit-hook: 2.2.1 805 | glob-to-regexp: 0.4.1 806 | stoppable: 1.1.0 807 | undici: 5.29.0 808 | workerd: 1.20250507.0 809 | ws: 8.18.0 810 | youch: 3.3.4 811 | zod: 3.22.3 812 | transitivePeerDependencies: 813 | - bufferutil 814 | - utf-8-validate 815 | 816 | mustache@4.2.0: {} 817 | 818 | ohash@2.0.11: {} 819 | 820 | path-to-regexp@6.3.0: {} 821 | 822 | pathe@2.0.3: {} 823 | 824 | printable-characters@1.0.42: {} 825 | 826 | semver@7.7.1: 827 | optional: true 828 | 829 | sharp@0.33.5: 830 | dependencies: 831 | color: 4.2.3 832 | detect-libc: 2.0.3 833 | semver: 7.7.1 834 | optionalDependencies: 835 | '@img/sharp-darwin-arm64': 0.33.5 836 | '@img/sharp-darwin-x64': 0.33.5 837 | '@img/sharp-libvips-darwin-arm64': 1.0.4 838 | '@img/sharp-libvips-darwin-x64': 1.0.4 839 | '@img/sharp-libvips-linux-arm': 1.0.5 840 | '@img/sharp-libvips-linux-arm64': 1.0.4 841 | '@img/sharp-libvips-linux-s390x': 1.0.4 842 | '@img/sharp-libvips-linux-x64': 1.0.4 843 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 844 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 845 | '@img/sharp-linux-arm': 0.33.5 846 | '@img/sharp-linux-arm64': 0.33.5 847 | '@img/sharp-linux-s390x': 0.33.5 848 | '@img/sharp-linux-x64': 0.33.5 849 | '@img/sharp-linuxmusl-arm64': 0.33.5 850 | '@img/sharp-linuxmusl-x64': 0.33.5 851 | '@img/sharp-wasm32': 0.33.5 852 | '@img/sharp-win32-ia32': 0.33.5 853 | '@img/sharp-win32-x64': 0.33.5 854 | optional: true 855 | 856 | simple-swizzle@0.2.2: 857 | dependencies: 858 | is-arrayish: 0.3.2 859 | optional: true 860 | 861 | source-map@0.6.1: {} 862 | 863 | stacktracey@2.1.8: 864 | dependencies: 865 | as-table: 1.0.55 866 | get-source: 2.0.12 867 | 868 | stoppable@1.1.0: {} 869 | 870 | tslib@2.8.1: 871 | optional: true 872 | 873 | typescript@5.8.3: {} 874 | 875 | ufo@1.6.1: {} 876 | 877 | undici@5.29.0: 878 | dependencies: 879 | '@fastify/busboy': 2.1.1 880 | 881 | unenv@2.0.0-rc.15: 882 | dependencies: 883 | defu: 6.1.4 884 | exsolve: 1.0.4 885 | ohash: 2.0.11 886 | pathe: 2.0.3 887 | ufo: 1.6.1 888 | 889 | workerd@1.20250507.0: 890 | optionalDependencies: 891 | '@cloudflare/workerd-darwin-64': 1.20250507.0 892 | '@cloudflare/workerd-darwin-arm64': 1.20250507.0 893 | '@cloudflare/workerd-linux-64': 1.20250507.0 894 | '@cloudflare/workerd-linux-arm64': 1.20250507.0 895 | '@cloudflare/workerd-windows-64': 1.20250507.0 896 | 897 | wrangler@4.14.4(@cloudflare/workers-types@4.20250409.0): 898 | dependencies: 899 | '@cloudflare/kv-asset-handler': 0.4.0 900 | '@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250507.0) 901 | blake3-wasm: 2.1.5 902 | esbuild: 0.25.4 903 | miniflare: 4.20250507.0 904 | path-to-regexp: 6.3.0 905 | unenv: 2.0.0-rc.15 906 | workerd: 1.20250507.0 907 | optionalDependencies: 908 | '@cloudflare/workers-types': 4.20250409.0 909 | fsevents: 2.3.3 910 | sharp: 0.33.5 911 | transitivePeerDependencies: 912 | - bufferutil 913 | - utf-8-validate 914 | 915 | ws@8.18.0: {} 916 | 917 | youch@3.3.4: 918 | dependencies: 919 | cookie: 0.7.2 920 | mustache: 4.2.0 921 | stacktracey: 2.1.8 922 | 923 | zod@3.22.3: {} 924 | -------------------------------------------------------------------------------- /http2/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from 'cloudflare:workers'; 2 | 3 | export class Container extends DurableObject { 4 | container: globalThis.Container; 5 | monitor?: Promise; 6 | 7 | constructor(ctx: DurableObjectState, env: Env) { 8 | super(ctx, env); 9 | this.container = ctx.container!; 10 | void this.ctx.blockConcurrencyWhile(async () => { 11 | if (!this.container.running) this.container.start(); 12 | }); 13 | } 14 | 15 | async fetch(req: Request) { 16 | try { 17 | return await this.container.getTcpPort(8080).fetch(req.url.replace('https:', 'http:'), req); 18 | } catch (err) { 19 | return new Response(`${this.ctx.id.toString()}: ${err.message}`, { status: 500 }); 20 | } 21 | } 22 | } 23 | 24 | export default { 25 | async fetch(request, env): Promise { 26 | try { 27 | return await env.CONTAINER.get(env.CONTAINER.idFromName('fetcher')).fetch(request); 28 | } catch (err) { 29 | console.error('Error fetch:', err.message); 30 | return new Response(err.message, { status: 500 }); 31 | } 32 | }, 33 | } satisfies ExportedHandler; 34 | -------------------------------------------------------------------------------- /http2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /http2/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "http2", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "Container", 10 | ], 11 | "tag": "v1" 12 | } 13 | ], 14 | "containers": [{ 15 | "name": "http2", 16 | "image": "./Dockerfile", 17 | "class_name": "Container", 18 | "max_instances": 2 19 | }], 20 | "durable_objects": { 21 | "bindings": [ 22 | { 23 | "class_name": "Container", 24 | "name": "CONTAINER" 25 | }, 26 | ] 27 | }, 28 | "observability": { 29 | "enabled": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /load-balancer/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /load-balancer/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /load-balancer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /load-balancer/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /load-balancer/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.23 AS build 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY container/go.mod ./ 9 | RUN go mod download 10 | 11 | # Copy container src 12 | COPY container/*.go ./ 13 | # Build 14 | RUN CGO_ENABLED=0 GOOS=linux go build -o /server 15 | 16 | FROM scratch 17 | COPY --from=build /server /server 18 | EXPOSE 8080 19 | # Run 20 | CMD ["/server"] 21 | -------------------------------------------------------------------------------- /load-balancer/README.md: -------------------------------------------------------------------------------- 1 | # Load Balancer with KV 2 | 3 | This example showcases how would you build a load balancer with DO containers and KV. 4 | 5 | 1. There is a central container manager that is used to poll containers and scale up/down through the API. 6 | 1. Once a container is healthy, it will add itself to the KV pool. 7 | 1. Once a container gets signalled, it will fail its healthchecks, which will make it remove itself from the KV pool. 8 | 1. If you hit `/lb`, the request will be load balanced across the available keys in KV. 9 | 10 | -------------------------------------------------------------------------------- /load-balancer/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /load-balancer/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | func handler(w http.ResponseWriter, r *http.Request) { 14 | country := os.Getenv("CLOUDFLARE_COUNTRY_A2") 15 | location := os.Getenv("CLOUDFLARE_LOCATION") 16 | region := os.Getenv("CLOUDFLARE_REGION") 17 | 18 | fmt.Fprintf(w, "Hi, I'm a container running in %s, %s, which is part of %s ", location, country, region) 19 | } 20 | 21 | func main() { 22 | c := make(chan os.Signal, 10) 23 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 24 | terminate := false 25 | go func() { 26 | for range c { 27 | if terminate { 28 | os.Exit(0) 29 | continue 30 | } 31 | 32 | terminate = true 33 | go func() { 34 | time.Sleep(time.Minute) 35 | os.Exit(0) 36 | }() 37 | } 38 | }() 39 | 40 | http.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { 41 | if terminate { 42 | w.WriteHeader(400) 43 | w.Write([]byte("draining")) 44 | return 45 | } 46 | 47 | w.Write([]byte("ok")) 48 | }) 49 | 50 | http.HandleFunc("/", handler) 51 | log.Fatal(http.ListenAndServe(":8080", nil)) 52 | } 53 | -------------------------------------------------------------------------------- /load-balancer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "load-balancer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /load-balancer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from 'cloudflare:workers'; 2 | 3 | // starting => we called start() and init the monitor promise 4 | // running => container returned healthy on the endpoint 5 | // unhealthy => container is unhealthy (returning not OK status codes) 6 | // stopped => container is stopped (finished running) 7 | // failed => container failed to run and it won't try to run again, unless called 'start' again 8 | type ContainerState = 'starting' | 'running' | 'unhealthy' | 'stopped' | 'failed'; 9 | 10 | async function wrap(fn: Promise): Promise<[T, null] | [null, E]> { 11 | return fn.then((data) => [data, null] as [T, null]).catch((err) => [null, err as unknown as E] as [null, E]); 12 | } 13 | 14 | function isNotListeningError(err: Error): boolean { 15 | return err.message.includes('the container is not listening'); 16 | } 17 | 18 | function noContainerYetError(err: Error): boolean { 19 | return err.message.includes('there is no container instance'); 20 | } 21 | 22 | export class Container extends DurableObject { 23 | container: globalThis.Container; 24 | monitor?: Promise; 25 | 26 | async state(): Promise { 27 | return (await this.ctx.storage.get('state')) ?? 'starting'; 28 | } 29 | 30 | async stateTx(cb: (state: ContainerState) => Promise) { 31 | return await this.ctx.blockConcurrencyWhile(async () => { 32 | const s = await this.state(); 33 | await cb(s); 34 | }); 35 | } 36 | 37 | private async setState(state: ContainerState) { 38 | console.log('Setting container state', state); 39 | await this.ctx.storage.put('state', state); 40 | await this.ctx.storage.sync(); 41 | } 42 | 43 | constructor(ctx: DurableObjectState, env: Env) { 44 | if (ctx.container === undefined) { 45 | throw new Error('container is not defined'); 46 | } 47 | 48 | super(ctx, env); 49 | this.container = ctx.container; 50 | this.ctx.blockConcurrencyWhile(async () => { 51 | if (this.container.running) { 52 | if (this.monitor === undefined) { 53 | this.monitor = this.container.monitor(); 54 | this.handleMonitorPromise(this.monitor); 55 | } 56 | } else { 57 | await this.env.LOAD_BALANCER_STATE.delete(this.ctx.id.toString()); 58 | await this.setState('stopped'); 59 | } 60 | 61 | // if no alarm, trigger ASAP 62 | await this.setAlarm(Date.now()); 63 | }); 64 | } 65 | 66 | async setAlarm(value = Date.now() + 500) { 67 | const alarm = await this.ctx.storage.getAlarm(); 68 | if (alarm === null) { 69 | await this.ctx.storage.setAlarm(value); 70 | await this.ctx.storage.sync(); 71 | } 72 | } 73 | 74 | async fetch(req: Request) { 75 | const url = new URL(req.url.replace('https:', 'http:')); 76 | try { 77 | return await this.container.getTcpPort(8080).fetch(url, req); 78 | } catch (err) { 79 | if (err instanceof Error) console.error('Error getting TCP port 8080:', err.message); 80 | else throw err; 81 | await this.setAlarm(Date.now()); 82 | return new Response('service is unreachable right now', { status: 500 }); 83 | } 84 | } 85 | 86 | async alarm() { 87 | try { 88 | const recalculateState = async () => { 89 | const state = await this.state(); 90 | if (state === 'stopped') { 91 | // TODO: GC itself 92 | return; 93 | } 94 | 95 | const [result, err] = await wrap(this.healthCheck()); 96 | if (err !== null) { 97 | console.error('Received an internal error from healthCheck:', err.message); 98 | if (state !== 'starting') { 99 | await this.setState('failed'); 100 | } 101 | 102 | return; 103 | } 104 | 105 | if (typeof result !== 'string') { 106 | console.warn('Container is unhealthy because it returned a ', result.status); 107 | 108 | // consume text stream 109 | await wrap(result.text()); 110 | 111 | await this.setState('unhealthy'); 112 | return; 113 | } 114 | 115 | console.log('Container got a result from healthcheck:', result); 116 | if (result === 'ok') { 117 | await this.setState('running'); 118 | return; 119 | } 120 | 121 | if (result == 'not_listening' || result == 'no_container_yet') { 122 | await this.setState('starting'); 123 | return; 124 | } 125 | 126 | console.error('unknown result:', result); 127 | }; 128 | 129 | await recalculateState(); 130 | const state = await this.state(); 131 | 132 | const stop = async () => { 133 | this.env.LOAD_BALANCER_STATE.delete(this.ctx.id.toString()); 134 | if (this.container.running) this.container.signal(15); 135 | await this.setState('stopped'); 136 | }; 137 | 138 | const instances = await this.ctx.storage.get('instance'); 139 | if (instances === undefined) { 140 | await stop(); 141 | return; 142 | } 143 | 144 | const key = await this.env.LOAD_BALANCER_STATE.get(this.ctx.id.toString()); 145 | if (key === null && state === 'running') { 146 | console.log("Adding to load balanced as it's running"); 147 | await this.env.LOAD_BALANCER_STATE.put(this.ctx.id.toString(), `${Date.now()}`); 148 | } else if (state !== 'running') { 149 | console.log("Removing from load balanced as it's not running:", state); 150 | await this.env.LOAD_BALANCER_STATE.delete(this.ctx.id.toString()); 151 | } 152 | 153 | const manager = this.env.CONTAINER_MANAGER.get(this.env.CONTAINER_MANAGER.idFromName('manager')); 154 | if (!(await manager.shouldIKeepRunning(instances))) { 155 | await stop(); 156 | } 157 | } finally { 158 | await this.setAlarm(); 159 | } 160 | } 161 | 162 | handleMonitorPromise(monitor: Promise) { 163 | monitor 164 | .then(async () => { 165 | await this.stateTx(async (state) => { 166 | if (state === 'running' || state == 'unhealthy') { 167 | await this.setState('stopped'); 168 | console.log(`Container stopped from state ${state}`); 169 | return; 170 | } 171 | 172 | if (state === 'starting') { 173 | console.log('Container was starting, and monitor resolved, we might have had an exception, retrying later'); 174 | return; 175 | } 176 | 177 | if (state === 'failed') { 178 | console.log('Container was marked as failed, but we resolved monitor successfully'); 179 | } 180 | }); 181 | }) 182 | .catch(async (err) => { 183 | console.error(`Monitor exited with an error: ${err.message}`); 184 | await this.setState('failed'); 185 | }); 186 | } 187 | 188 | // 'start' will start the container, and it will make sure it runs until the end 189 | async start(instance: number, containerStart?: ContainerStartupOptions) { 190 | console.log('Calling start on container:', this.container.running); 191 | if (this.container.running) { 192 | if (this.monitor === undefined) { 193 | this.monitor = this.container.monitor(); 194 | this.handleMonitorPromise(this.monitor); 195 | } 196 | 197 | return; 198 | } 199 | 200 | await this.ctx.storage.put('instance', instance); 201 | await this.ctx.storage.sync(); 202 | 203 | await this.setState('starting'); 204 | this.container.start(containerStart); 205 | this.monitor = this.container.monitor(); 206 | this.handleMonitorPromise(this.monitor); 207 | } 208 | 209 | // This ALWAYS throws an exception because it resets the DO 210 | async destroy() { 211 | try { 212 | await this.ctx.storage.deleteAll(); 213 | await this.ctx.storage.deleteAlarm(); 214 | await this.container.destroy(); 215 | } finally { 216 | this.ctx.abort(); 217 | } 218 | } 219 | 220 | // healthCheck returns 'ok' when the container returned 221 | // in the port returned a successful status code. 222 | // It will return a Response object when the status code is not ok. 223 | // It will return a known error enum if the container is not ready yet. 224 | async healthCheck(portNumber = 8080): Promise<'ok' | 'not_listening' | 'no_container_yet' | Response> { 225 | const port = this.container.getTcpPort(portNumber); 226 | const [res, err] = await wrap(port.fetch(new Request('http://container/_health'))); 227 | if (err !== null) { 228 | if (isNotListeningError(err)) { 229 | return 'not_listening'; 230 | } 231 | 232 | if (noContainerYetError(err)) { 233 | return 'no_container_yet'; 234 | } 235 | 236 | // :( 237 | throw err; 238 | } 239 | 240 | if (res.ok) { 241 | await res.text(); 242 | return 'ok'; 243 | } 244 | 245 | // let the end user handle the not ok status code 246 | return res; 247 | } 248 | } 249 | 250 | export class ContainerManager extends DurableObject { 251 | constructor(ctx: DurableObjectState, env: Env) { 252 | super(ctx, env); 253 | this.ctx.storage.setAlarm(Date.now()); 254 | } 255 | 256 | async setNumberOfInstances(instances: number) { 257 | await this.ctx.storage.put('instances', instances); 258 | await this.ctx.storage.setAlarm(Date.now()); 259 | } 260 | 261 | async shouldIKeepRunning(containerInstance: number) { 262 | const instances = (await this.ctx.storage.get('instances')) ?? 0; 263 | if (instances <= containerInstance) return false; 264 | return true; 265 | } 266 | 267 | containerId(instance: number): DurableObjectId { 268 | return this.env.CONTAINER.idFromName(`instance-${instance}`); 269 | } 270 | 271 | async alarm() { 272 | try { 273 | await this.ctx.blockConcurrencyWhile(async () => { 274 | const instances = (await this.ctx.storage.get('instances')) ?? 0; 275 | for (let instance = 0; instance < instances; instance++) { 276 | const containerId = this.containerId(instance); 277 | const container = this.env.CONTAINER.get(containerId); 278 | const [state, err] = await wrap(container.state()); 279 | console.log('Container instance', instance, 'belongs to actor id', containerId.toString()); 280 | 281 | if (err !== null) { 282 | console.error('Container instance', instance, 'threw an error', containerId.toString(), err.message); 283 | continue; 284 | } 285 | 286 | if (state === 'failed') { 287 | console.warn('Container', instance, 'returned failed'); 288 | } 289 | 290 | if (state === 'stopped') { 291 | console.log('Container', instance, 'is stopped'); 292 | } 293 | 294 | if (state === 'starting' || state === 'failed' || state === 'stopped') { 295 | const [, err] = await wrap(container.start(instance)); 296 | if (err !== null) console.error('Container instance start()', instance, 'threw an error', containerId.toString(), err.message); 297 | console.log('Container', instance, 'started'); 298 | continue; 299 | } 300 | 301 | if (state === 'unhealthy') { 302 | console.warn('Container', instance, 'is unhealthy right now'); 303 | } 304 | 305 | if (state === 'running') { 306 | console.log('Container', instance, 'is ok'); 307 | } 308 | } 309 | }); 310 | } finally { 311 | await this.ctx.storage.setAlarm(Date.now() + 1000); 312 | } 313 | } 314 | 315 | async getContainerStates(): Promise<(ContainerState | 'unknown')[]> { 316 | const instances = (await this.ctx.storage.get('instances')) ?? 0; 317 | const statuses: (ContainerState | 'unknown')[] = []; 318 | 319 | // TODO: do this in parallel basis with concurrency limits 320 | for (let i = 0; i < instances; i++) { 321 | const stub = this.env.CONTAINER.get(this.containerId(i)); 322 | const [state, err] = await wrap(stub.state()); 323 | if (err !== null) { 324 | console.error(`Instance ${stub.id.toString()} (${i}) threw an error when hitting: ${err.message}`); 325 | statuses.push('unknown'); 326 | continue; 327 | } 328 | 329 | statuses.push(state); 330 | } 331 | 332 | return statuses; 333 | } 334 | } 335 | 336 | export default { 337 | async fetch(request, env): Promise { 338 | const url = new URL(request.url); 339 | const manager = env.CONTAINER_MANAGER.get(env.CONTAINER_MANAGER.idFromName('manager')); 340 | 341 | if (url.pathname.includes('/statuses')) { 342 | try { 343 | const containers = await env.LOAD_BALANCER_STATE.list(); 344 | const states = await manager.getContainerStates(); 345 | return Response.json({ states, load_balancer: containers.keys }); 346 | } catch (err) { 347 | if (!(err instanceof Error)) throw err; 348 | 349 | return new Response(err.message); 350 | } 351 | } 352 | 353 | if (url.pathname.includes('/lb')) { 354 | const containers = await env.LOAD_BALANCER_STATE.list(); 355 | if (containers.keys.length === 0) { 356 | return Response.json({ error: 'no containers are healthy' }, { status: 500 }); 357 | } 358 | 359 | const index = Math.floor(Math.random() * containers.keys.length); 360 | const key = containers.keys[index]; 361 | const containerId = env.CONTAINER.idFromString(key.name); 362 | 363 | // Ideally, we are able to redirect the request somewhere else 364 | // if this throws an internal error. 365 | const res = await env.CONTAINER.get(containerId).fetch(request); 366 | 367 | return res; 368 | } 369 | 370 | if (url.pathname.includes('/containers')) { 371 | const instancesString = url.pathname.split('/').pop(); 372 | if (instancesString === null || instancesString === undefined) return new Response('expected /containers/'); 373 | 374 | const instances = +instancesString; 375 | if (isNaN(instances)) return new Response('expected /containers/'); 376 | await manager.setNumberOfInstances(instances); 377 | return new Response('ok'); 378 | } 379 | 380 | return new Response( 381 | 'Hit /statuses if you want to list containers. Hit /containers/ to scale to a certain number of instances', 382 | ); 383 | }, 384 | } satisfies ExportedHandler; 385 | -------------------------------------------------------------------------------- /load-balancer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /load-balancer/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "load-balancer-2", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "Container", 10 | "ContainerManager" 11 | ], 12 | "tag": "v1" 13 | } 14 | ], 15 | "containers": [{ 16 | "name": "container-2", 17 | "image": "./Dockerfile", 18 | "class_name": "Container", 19 | "max_instances": 3 20 | }], 21 | "durable_objects": { 22 | "bindings": [ 23 | { 24 | "class_name": "Container", 25 | "name": "CONTAINER" 26 | }, 27 | { 28 | "class_name": "ContainerManager", 29 | "name": "CONTAINER_MANAGER" 30 | }, 31 | ] 32 | }, 33 | "kv_namespaces": [ 34 | { 35 | "binding": "LOAD_BALANCER_STATE", 36 | "id": "96b88e244c5c448a9895256729756161" 37 | } 38 | ], 39 | "observability": { 40 | "enabled": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sqlite/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /sqlite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /sqlite/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /sqlite/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /sqlite/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.23 AS build 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY container/go.mod ./ 9 | RUN go mod download 10 | 11 | # Copy container src 12 | COPY container/*.go ./ 13 | # Build 14 | RUN CGO_ENABLED=0 GOOS=linux go build -o /server 15 | 16 | FROM debian:latest 17 | COPY --from=build /server /server 18 | EXPOSE 8080 19 | # Run 20 | CMD ["/server"] 21 | -------------------------------------------------------------------------------- /sqlite/README.md: -------------------------------------------------------------------------------- 1 | # Simple job runner with SQLITE 2 | 3 | This example allows you to submit simple command line jobs that will be persisted in SQLITE and ran 4 | asynchronously in the container. 5 | 6 | -------------------------------------------------------------------------------- /sqlite/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /sqlite/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | func handler(w http.ResponseWriter, r *http.Request) { 17 | country := os.Getenv("CLOUDFLARE_COUNTRY_A2") 18 | location := os.Getenv("CLOUDFLARE_LOCATION") 19 | region := os.Getenv("CLOUDFLARE_REGION") 20 | 21 | fmt.Fprintf(w, "Hi, I'm a container running in %s, %s, which is part of %s ", location, country, region) 22 | } 23 | 24 | func main() { 25 | c := make(chan os.Signal, 10) 26 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 27 | terminate := false 28 | go func() { 29 | for range c { 30 | if terminate { 31 | os.Exit(0) 32 | continue 33 | } 34 | 35 | terminate = true 36 | go func() { 37 | time.Sleep(time.Minute) 38 | os.Exit(0) 39 | }() 40 | } 41 | }() 42 | 43 | http.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { 44 | if terminate { 45 | w.WriteHeader(400) 46 | w.Write([]byte("draining")) 47 | return 48 | } 49 | 50 | w.Write([]byte("ok")) 51 | }) 52 | 53 | http.HandleFunc("/exec", func(w http.ResponseWriter, r *http.Request) { 54 | text, err := io.ReadAll(r.Body) 55 | if err != nil { 56 | w.WriteHeader(500) 57 | return 58 | } 59 | 60 | cmdString := strings.Split(string(text), " ") 61 | cmd := exec.Command(cmdString[0], func() []string { 62 | if len(cmdString) == 1 { 63 | return []string{} 64 | } 65 | 66 | return cmdString[1:] 67 | }()...) 68 | output, err := cmd.CombinedOutput() 69 | if err != nil { 70 | w.WriteHeader(400) 71 | } 72 | 73 | w.Write([]byte("output: ")) 74 | w.Write(output) 75 | if err != nil { 76 | w.Write([]byte{'\n'}) 77 | w.Write([]byte(err.Error())) 78 | } 79 | }) 80 | 81 | http.HandleFunc("/", handler) 82 | log.Fatal(http.ListenAndServe(":8080", nil)) 83 | } 84 | -------------------------------------------------------------------------------- /sqlite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sqlite/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from 'cloudflare:workers'; 2 | 3 | async function wrap(fn: Promise): Promise<[T, null] | [null, E]> { 4 | return fn.then((data) => [data, null] as [T, null]).catch((err) => [null, err as unknown as E] as [null, E]); 5 | } 6 | 7 | type Job = { cmd: string; id: string; output: string; completed: boolean; active: boolean }; 8 | 9 | export class Container extends DurableObject { 10 | container: globalThis.Container; 11 | monitor?: Promise; 12 | 13 | constructor(ctx: DurableObjectState, env: Env) { 14 | super(ctx, env); 15 | this.container = ctx.container!; 16 | void this.ctx.blockConcurrencyWhile(async () => { 17 | this.ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS jobs (id TEXT, cmd TEXT, completed BOOLEAN, active BOOLEAN, output TEXT);'); 18 | 19 | if (this.container.running) { 20 | if (this.monitor === undefined) { 21 | this.monitor = this.container.monitor(); 22 | this.handleMonitorPromise(this.monitor); 23 | } 24 | } else { 25 | this.container.start({ enableInternet: true }); 26 | this.monitor = this.container.monitor(); 27 | this.handleMonitorPromise(this.monitor); 28 | } 29 | 30 | // if no alarm, trigger ASAP 31 | await this.setAlarm(Date.now()); 32 | }); 33 | } 34 | 35 | async setAlarm(value = Date.now() + 500) { 36 | const alarm = await this.ctx.storage.getAlarm(); 37 | if (alarm === null) { 38 | await this.ctx.storage.setAlarm(value); 39 | await this.ctx.storage.sync(); 40 | } 41 | } 42 | 43 | inflightFetch?: Promise; 44 | async alarm() { 45 | try { 46 | if (this.inflightFetch !== undefined) { 47 | return; 48 | } 49 | 50 | const currentJob = await this.getActiveJob(); 51 | if (currentJob === null) { 52 | const toRun = await this.getOnePendingJobAndMarkAsActive(); 53 | if (toRun === null) return; 54 | void this.run(toRun.id, toRun.cmd); 55 | return; 56 | } 57 | 58 | void this.run(currentJob.id, currentJob.cmd); 59 | } finally { 60 | await this.setAlarm(); 61 | } 62 | } 63 | 64 | run(id: string, cmd: string): Promise { 65 | this.inflightFetch = this.container 66 | .getTcpPort(8080) 67 | .fetch(new Request('http://container/exec', { body: cmd, method: 'POST' })) 68 | .then(async (res) => { 69 | const output = await res.text(); 70 | this.finishJob(id, `${res.status} ${output}`); 71 | await this.ctx.storage.sync(); 72 | this.inflightFetch = undefined; 73 | return res; 74 | }) 75 | .catch((err) => { 76 | console.error('Error running the job, we will need to retry:', err); 77 | this.inflightFetch = undefined; 78 | return; 79 | }) 80 | .finally(() => { 81 | this.inflightFetch = undefined; 82 | }); 83 | 84 | return this.inflightFetch; 85 | } 86 | 87 | private finishJob(id: string, output: string) { 88 | const jobs = this.ctx.storage.sql.exec( 89 | 'UPDATE jobs SET completed = true, active = false, output = ? WHERE id = ? RETURNING *', 90 | output, 91 | id, 92 | ); 93 | try { 94 | const row = jobs.one() as unknown as Job; 95 | return row; 96 | } catch { 97 | return null; 98 | } 99 | } 100 | 101 | async getOnePendingJobAndMarkAsActive(): Promise { 102 | const jobs = this.ctx.storage.sql.exec(`UPDATE jobs SET active = true WHERE rowid = ( 103 | SELECT MIN(rowid) as rowid 104 | FROM jobs 105 | WHERE active = false AND completed = false 106 | LIMIT 1 107 | ) RETURNING *;`); 108 | try { 109 | return jobs.one() as unknown as Job; 110 | } catch (err) { 111 | return null; 112 | } 113 | } 114 | 115 | async getActiveJob(): Promise { 116 | const jobs = this.ctx.storage.sql.exec('SELECT * FROM jobs WHERE active = true AND completed = false LIMIT 1;'); 117 | try { 118 | const row = jobs.one() as unknown as Job; 119 | return row; 120 | } catch { 121 | return null; 122 | } 123 | } 124 | 125 | async getJobs(): Promise { 126 | const jobs = this.ctx.storage.sql.exec('SELECT * FROM jobs;'); 127 | const row = jobs.toArray() as unknown; 128 | return row as Job[]; 129 | } 130 | 131 | async submitJob(cmd: string[]): Promise { 132 | const id = crypto.randomUUID(); 133 | this.ctx.storage.sql.exec( 134 | 'INSERT INTO jobs (id, cmd, completed, active, output) VALUES (?, ?, ?, ?, ?)', 135 | id, 136 | cmd.join(' '), 137 | 0, 138 | 0, 139 | null, 140 | ); 141 | return id; 142 | } 143 | 144 | handleMonitorPromise(monitor: Promise) { 145 | monitor 146 | .then(async () => { 147 | console.log('Container exited'); 148 | }) 149 | .catch(async (err) => { 150 | console.error(`Monitor exited with an error: ${err.message}`); 151 | }) 152 | .finally(async () => { 153 | await this.setAlarm(); 154 | this.ctx.abort(); 155 | }); 156 | } 157 | } 158 | 159 | export default { 160 | async fetch(request, env): Promise { 161 | const runner = env.CONTAINER.get(env.CONTAINER.idFromName('runner')); 162 | if (request.method === 'GET') { 163 | const jobs = await runner.getJobs(); 164 | return Response.json(jobs); 165 | } 166 | 167 | const cmd = await request.text(); 168 | await runner.submitJob(cmd.split(' ')); 169 | return new Response('ok'); 170 | }, 171 | } satisfies ExportedHandler; 172 | -------------------------------------------------------------------------------- /sqlite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sqlite/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "job-runner", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "Container", 10 | ], 11 | "tag": "v1" 12 | } 13 | ], 14 | "containers": [{ 15 | "name": "runner", 16 | "image": "./Dockerfile", 17 | "class_name": "Container", 18 | "max_instances": 10 19 | }], 20 | "durable_objects": { 21 | "bindings": [ 22 | { 23 | "class_name": "Container", 24 | "name": "CONTAINER" 25 | }, 26 | ] 27 | }, 28 | "observability": { 29 | "enabled": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /terminal/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | -------------------------------------------------------------------------------- /terminal/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /terminal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS builder 2 | RUN apk add --no-cache python3 make g++ 3 | RUN npm install -g pnpm 4 | WORKDIR /usr/src/app 5 | COPY host/package.json host/pnpm-lock.yaml ./ 6 | RUN pnpm install 7 | COPY host/server.js ./ 8 | 9 | FROM node:18-alpine AS runtime 10 | RUN npm install -g pnpm 11 | WORKDIR /usr/src/app 12 | ENV NODE_ENV=production 13 | COPY host/package.json host/pnpm-lock.yaml ./ 14 | COPY --from=builder /usr/src/app/node_modules ./node_modules 15 | COPY --from=builder /usr/src/app/server.js ./ 16 | EXPOSE 8080 17 | USER node 18 | CMD [ "node", "server.js" ] 19 | -------------------------------------------------------------------------------- /terminal/README.md: -------------------------------------------------------------------------------- 1 | Terminal 2 | 3 | This examples creates uses xterm.js and proxies it to a running container. 4 | -------------------------------------------------------------------------------- /terminal/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pty-websocket-host", 3 | "version": "1.0.0", 4 | "description": "Node.js PTY WebSocket host for xterm.js", 5 | "main": "server.js", 6 | "pnpm": { 7 | "onlyBuiltDependencies": [ 8 | "node-pty" 9 | ] 10 | }, 11 | "scripts": { 12 | "start": "node server.js" 13 | }, 14 | "dependencies": { 15 | "node-pty": "^1.0.0", 16 | "ws": "^8.17.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /terminal/host/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | node-pty: 12 | specifier: ^1.0.0 13 | version: 1.0.0 14 | ws: 15 | specifier: ^8.17.0 16 | version: 8.18.2 17 | 18 | packages: 19 | 20 | nan@2.22.2: 21 | resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} 22 | 23 | node-pty@1.0.0: 24 | resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==} 25 | 26 | ws@8.18.2: 27 | resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} 28 | engines: {node: '>=10.0.0'} 29 | peerDependencies: 30 | bufferutil: ^4.0.1 31 | utf-8-validate: '>=5.0.2' 32 | peerDependenciesMeta: 33 | bufferutil: 34 | optional: true 35 | utf-8-validate: 36 | optional: true 37 | 38 | snapshots: 39 | 40 | nan@2.22.2: {} 41 | 42 | node-pty@1.0.0: 43 | dependencies: 44 | nan: 2.22.2 45 | 46 | ws@8.18.2: {} 47 | -------------------------------------------------------------------------------- /terminal/host/server.js: -------------------------------------------------------------------------------- 1 | const os = require("os"); 2 | const pty = require("node-pty"); // Pseudo-terminal spawner 3 | const WebSocket = require("ws"); // WebSocket library 4 | 5 | const PORT = process.env.PORT || 8080; // Port for the WebSocket server 6 | const WS_PATH = "/terminal"; // Path for WebSocket connections 7 | 8 | // --- WebSocket Server Setup --- 9 | const wss = new WebSocket.Server({ port: PORT, path: WS_PATH }); 10 | 11 | console.log(`🚀 WebSocket server started on ws://localhost:${PORT}${WS_PATH}`); 12 | if (os.platform() !== "win32" && process.getuid && process.getuid() === 0) { 13 | console.warn( 14 | "\x1b[33m⚠️ WARNING: Server is running as root. This is not recommended for production.\x1b[0m", 15 | ); 16 | } 17 | console.log("Waiting for client connections..."); 18 | 19 | wss.on("connection", (ws, req) => { 20 | const clientIp = req.socket.remoteAddress; 21 | console.log(`\n🔗 Client connected: ${clientIp}`); 22 | 23 | // --- PTY Process Setup --- 24 | // Determine the shell based on the OS 25 | const shell = 26 | os.platform() === "win32" ? "powershell.exe" : process.env.SHELL || "sh"; 27 | const ptyProcess = pty.spawn(shell, [], { 28 | name: "xterm-256color", // Terminal type 29 | cols: 80, // Initial columns 30 | rows: 30, // Initial rows 31 | cwd: process.env.HOME || process.env.USERPROFILE, // User's home directory 32 | env: { ...process.env, LANG: "en_US.UTF-8" }, // Ensure UTF-8 for proper character display 33 | }); 34 | 35 | console.log( 36 | ` ↳ PTY process created for ${clientIp} (PID: ${ptyProcess.pid}, Shell: ${shell})`, 37 | ); 38 | 39 | // --- Data Flow: WebSocket -> PTY --- 40 | ws.on("message", (message) => { 41 | try { 42 | // The message from xterm-addon-attach is what the user types. 43 | // It can be a string or Buffer. node-pty's write method handles both. 44 | ptyProcess.write(message); 45 | } catch (e) { 46 | console.error(`Error writing to PTY for ${clientIp}:`, e); 47 | return; 48 | } 49 | }); 50 | 51 | // --- Data Flow: PTY -> WebSocket --- 52 | ptyProcess.onData((data) => { 53 | try { 54 | // Send data from PTY (shell output) to the WebSocket client 55 | if (ws.readyState === WebSocket.OPEN) { 56 | ws.send(data); 57 | } 58 | } catch (e) { 59 | // This can happen if the WebSocket closes abruptly. 60 | console.error( 61 | `Error sending PTY data to WebSocket for ${clientIp}:`, 62 | e.message, 63 | ); 64 | } 65 | }); 66 | 67 | // --- PTY Process Exit --- 68 | ptyProcess.onExit(({ exitCode, signal }) => { 69 | console.log( 70 | ` ↳ PTY process for ${clientIp} (PID: ${ptyProcess.pid}) exited. Code: ${exitCode}, Signal: ${signal}`, 71 | ); 72 | if (ws.readyState === WebSocket.OPEN) { 73 | ws.send( 74 | `\\r\\n\\x1b[31mShell process exited (Code: ${exitCode || "N/A"}, Signal: ${signal || "N/A"}). Session terminated.\\x1b[0m\\r\\n`, 75 | ); 76 | ws.close(1000, `PTY exited. Code: ${exitCode}, Signal: ${signal}`); 77 | } 78 | }); 79 | 80 | // --- WebSocket Close --- 81 | ws.on("close", (code, reason) => { 82 | console.log( 83 | `🔌 Client disconnected: ${clientIp}. Code: ${code}, Reason: ${reason || "N/A"}`, 84 | ); 85 | // Clean up the PTY process when the WebSocket connection closes 86 | if (ptyProcess && ptyProcess.pid && !ptyProcess.killed) { 87 | try { 88 | ptyProcess.kill(); 89 | console.log( 90 | ` ↳ Killed PTY process for ${clientIp} (PID: ${ptyProcess.pid}) due to WebSocket close.`, 91 | ); 92 | } catch (e) { 93 | console.error(`Error killing PTY for ${clientIp}:`, e); 94 | } 95 | } 96 | }); 97 | 98 | // --- WebSocket Error --- 99 | ws.on("error", (error) => { 100 | console.error(`WebSocket error for client ${clientIp}:`, error); 101 | // ptyProcess cleanup will be handled by 'close' event which usually follows 'error' 102 | }); 103 | }); 104 | 105 | // Graceful shutdown 106 | const shutdown = (signal) => { 107 | console.log(`\n${signal} received. Shutting down server...`); 108 | wss.close(() => { 109 | console.log("WebSocket server closed."); 110 | // Give PTYs a moment to be cleaned up by their respective ws.on('close') handlers 111 | setTimeout(() => { 112 | console.log("Exiting."); 113 | process.exit(0); 114 | }, 500); 115 | }); 116 | }; 117 | 118 | process.on("SIGINT", () => shutdown("SIGINT")); 119 | process.on("SIGTERM", () => shutdown("SIGTERM")); 120 | -------------------------------------------------------------------------------- /terminal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "deploy": "wrangler deploy" 4 | }, 5 | "dependencies": { 6 | "cf-containers": "^0.0.8", 7 | "node-pty": "^1.0.0", 8 | "ws": "^8.17.0" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "^4.14.4", 12 | "typescript": "^5.5.2" 13 | }, 14 | "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" 15 | } 16 | -------------------------------------------------------------------------------- /terminal/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Container, getContainer } from "cf-containers"; 2 | import html from "./terminal.html"; 3 | 4 | export class TerminalContainer extends Container { 5 | defaultPort = 8080; 6 | sleepAfter = "1h"; 7 | enableInternet = true; 8 | 9 | async fetch(request: Request): Promise { 10 | return await this.containerFetch(request, this.defaultPort); 11 | } 12 | } 13 | 14 | export default { 15 | async fetch( 16 | request: Request, 17 | env: { TERMINAL: DurableObjectNamespace }, 18 | ): Promise { 19 | const url = new URL(request.url); 20 | if (url.pathname === "/terminal") { 21 | return await getContainer(env.TERMINAL).fetch(request); 22 | } else if (url.pathname === "/" || url.pathname === "/index.html") { 23 | return new Response(html, { 24 | headers: { 25 | "content-type": "text/html;charset=UTF-8", 26 | }, 27 | }); 28 | } 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /terminal/src/terminal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Xterm.js via Cloudflare Worker (Proxied) 7 | 11 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 147 | 148 | 149 | `; 150 | -------------------------------------------------------------------------------- /terminal/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "shell", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "compatibility_flags": ["nodejs_compat"], 7 | "migrations": [ 8 | { 9 | "new_sqlite_classes": ["TerminalContainer"], 10 | "tag": "v1", 11 | }, 12 | ], 13 | "containers": [ 14 | { 15 | "name": "terminal-websockets", 16 | "image": "./Dockerfile", 17 | "class_name": "TerminalContainer", 18 | "max_instances": 1, 19 | "configuration": { 20 | "vcpu": 2, 21 | "memory_mib": 256, 22 | }, 23 | }, 24 | ], 25 | "durable_objects": { 26 | "bindings": [ 27 | { 28 | "class_name": "TerminalContainer", 29 | "name": "TERMINAL", 30 | }, 31 | ], 32 | }, 33 | "observability": { 34 | "enabled": true, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /websockets/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /websockets/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /websockets/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /websockets/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /websockets/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | COPY ./container/server /server 3 | EXPOSE 8080 4 | # Run 5 | CMD ["/server"] 6 | -------------------------------------------------------------------------------- /websockets/README.md: -------------------------------------------------------------------------------- 1 | # Websockets example 2 | 3 | This example showcases how you can open WebSockets from a DO to a container. 4 | 5 | You can also send a standard WebSocket request from the Worker to the Container, without changing anything, 6 | and the client will connect to the container. 7 | 8 | -------------------------------------------------------------------------------- /websockets/container/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23 4 | 5 | require github.com/gorilla/websocket v1.5.3 // indirect 6 | -------------------------------------------------------------------------------- /websockets/container/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 2 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | -------------------------------------------------------------------------------- /websockets/container/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var upgrader = websocket.Upgrader{} 13 | 14 | func ws(w http.ResponseWriter, r *http.Request) { 15 | c, _ := upgrader.Upgrade(w, r, nil) 16 | defer c.Close() 17 | for { 18 | _, msg, err := c.ReadMessage() 19 | if err != nil { 20 | break 21 | } 22 | 23 | c.WriteMessage(websocket.TextMessage, []byte("you said: "+string(msg))) 24 | } 25 | } 26 | 27 | func handler(w http.ResponseWriter, r *http.Request) { 28 | country := os.Getenv("CLOUDFLARE_COUNTRY_A2") 29 | location := os.Getenv("CLOUDFLARE_LOCATION") 30 | region := os.Getenv("CLOUDFLARE_REGION") 31 | 32 | fmt.Fprintf(w, "Hi, I'm a container running in %s, %s, which is part of %s ", location, country, region) 33 | } 34 | 35 | func main() { 36 | http.HandleFunc("/", handler) 37 | http.HandleFunc("/ws", ws) 38 | log.Fatal(http.ListenAndServe(":8080", nil)) 39 | } 40 | -------------------------------------------------------------------------------- /websockets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websockets", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "^4.14.4", 13 | "@cloudflare/workers-types": "^4.20250403.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /websockets/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from 'cloudflare:workers'; 2 | 3 | export class Container extends DurableObject { 4 | container: globalThis.Container; 5 | 6 | async blockConcurrencyRetry(cb: () => Promise) { 7 | await this.ctx.blockConcurrencyWhile(async () => { 8 | let lastErr; 9 | for (let i = 0; i < 10; i++) { 10 | try { 11 | return await cb(); 12 | } catch (err) { 13 | lastErr = err; 14 | continue; 15 | } 16 | } 17 | 18 | throw lastErr; 19 | }); 20 | } 21 | 22 | constructor(ctx: DurableObjectState, env: Env) { 23 | super(ctx, env); 24 | this.container = ctx.container!; 25 | this.blockConcurrencyRetry(async () => { 26 | await this.initWebsocket(); 27 | }); 28 | } 29 | 30 | conn?: WebSocket; 31 | async initWebsocket() { 32 | if (!this.container.running) this.container.start(); 33 | 34 | const res = await this.container.getTcpPort(8080).fetch(new Request('http://container/ws', { headers: { Upgrade: 'websocket' } })); 35 | if (res.webSocket === null) throw new Error('websocket server is faulty'); 36 | 37 | // Accept the websocket and listen to messages 38 | res.webSocket.accept(); 39 | res.webSocket.addEventListener('message', (msg) => { 40 | if (this.resolveResolve !== undefined) 41 | this.resolveResolve(typeof msg.data === 'string' ? msg.data : new TextDecoder().decode(msg.data)); 42 | }); 43 | 44 | res.webSocket.addEventListener('close', () => { 45 | this.ctx.abort(); 46 | }); 47 | 48 | this.conn = res.webSocket; 49 | } 50 | 51 | promise?: Promise; 52 | resolveResolve?: (s: string) => void; 53 | async send(message: string) { 54 | // add a promise to the class and send a message 55 | this.promise = new Promise((res) => { 56 | this.resolveResolve = res; 57 | }); 58 | 59 | this.conn?.send(message); 60 | } 61 | 62 | async receive(): Promise { 63 | if (this.promise !== undefined) return await this.promise; 64 | return ''; 65 | } 66 | 67 | async fetch(req: Request) { 68 | const url = req.url.replace('https:', 'http:'); 69 | return this.container.getTcpPort(8080).fetch(url, req); 70 | } 71 | } 72 | 73 | export default { 74 | async fetch(request, env): Promise { 75 | const id: DurableObjectId = env.CONTAINER.idFromName('foo'); 76 | const stub = env.CONTAINER.get(id); 77 | if (request.method !== 'POST') { 78 | return await stub.fetch(request); 79 | } 80 | 81 | await stub.send('we sent: ' + (await request.text())); 82 | const message = await stub.receive(); 83 | return new Response(message); 84 | }, 85 | } satisfies ExportedHandler; 86 | -------------------------------------------------------------------------------- /websockets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "target": "es2021", 7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 8 | "lib": ["es2021"], 9 | /* Specify what JSX code is generated. */ 10 | "jsx": "react-jsx", 11 | 12 | /* Specify what module code is generated. */ 13 | "module": "es2022", 14 | /* Specify how TypeScript looks up a file from a given module specifier. */ 15 | "moduleResolution": "node", 16 | /* Specify type package names to be included without being referenced in a source file. */ 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ], 20 | /* Enable importing .json files */ 21 | "resolveJsonModule": true, 22 | 23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 24 | "allowJs": true, 25 | /* Enable error reporting in type-checked JavaScript files. */ 26 | "checkJs": false, 27 | 28 | /* Disable emitting files from a compilation. */ 29 | "noEmit": true, 30 | 31 | /* Ensure that each file can be safely transpiled without relying on other imports. */ 32 | "isolatedModules": true, 33 | /* Allow 'import x from y' when a module doesn't have a default export. */ 34 | "allowSyntheticDefaultImports": true, 35 | /* Ensure that casing is correct in imports. */ 36 | "forceConsistentCasingInFileNames": true, 37 | 38 | /* Enable all strict type-checking options. */ 39 | "strict": true, 40 | 41 | /* Skip type checking all .d.ts files. */ 42 | "skipLibCheck": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /websockets/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "websockets", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-03", 6 | "migrations": [ 7 | { 8 | "new_sqlite_classes": [ 9 | "Container", 10 | ], 11 | "tag": "v1" 12 | } 13 | ], 14 | "containers": [{ 15 | "name": "container-websockets", 16 | "image": "./Dockerfile", 17 | "class_name": "Container", 18 | "max_instances": 3 19 | }], 20 | "durable_objects": { 21 | "bindings": [ 22 | { 23 | "class_name": "Container", 24 | "name": "CONTAINER" 25 | }, 26 | ] 27 | }, 28 | "observability": { 29 | "enabled": true 30 | } 31 | } 32 | --------------------------------------------------------------------------------