From 0a0b0c937400ba554c4550d7094bcf279675f3e2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 31 Oct 2026 16:26:41 +0600 Subject: [PATCH 1/3] feat(jam): replace MQTT with a Durable Object room server and synced playback - Add jam-server/: a Cloudflare Worker with one Durable Object per room code. It tracks membership, host handoff and kicks/bans, keeps the queue for late joiners, routes guest commands to the host and serves the room clock. - Sync playback: devices estimate the server clock, the host sends position anchors in server time, guests follow by nudging playback rate (seeking only when far off) and stream the host's audio source when they share a plugin. - Seek, play/pause, skip and jump apply to the whole room. - Recover from network loss instead of crashing: socket failures reconnect with backoff, silent sockets are detected, and an expired room is recreated by its host. - Keep cost and battery low: anchors only on change (plus host drift and a 60 s heartbeat), kept in memory rather than storage, free runtime keep-alives once a minute, rare clock re-syncs, and no polling while paused or buffering. - Remove the MQTT broker settings and libraries. - Add sync unit tests, client/session integration tests (JAM_TEST_URL) and a network-loss test, plus a deploy workflow for the server. --- .github/workflows/jam-server-deploy.yml | 35 + AGENTS.md | 8 + composeApp/build.gradle.kts | 14 - .../spotube/spotube/media/PlaybackService.kt | 2 +- .../composeResources/values-ar/strings.xml | 30 +- .../composeResources/values-bn/strings.xml | 30 +- .../composeResources/values-de/strings.xml | 30 +- .../composeResources/values-es/strings.xml | 30 +- .../composeResources/values-fa/strings.xml | 30 +- .../composeResources/values-fr/strings.xml | 30 +- .../composeResources/values-hi/strings.xml | 30 +- .../composeResources/values-id/strings.xml | 30 +- .../composeResources/values-it/strings.xml | 30 +- .../composeResources/values-ja/strings.xml | 30 +- .../composeResources/values-ko/strings.xml | 30 +- .../composeResources/values-nl/strings.xml | 30 +- .../composeResources/values-pl/strings.xml | 30 +- .../composeResources/values-pt/strings.xml | 30 +- .../composeResources/values-ru/strings.xml | 30 +- .../composeResources/values-th/strings.xml | 30 +- .../composeResources/values-tr/strings.xml | 30 +- .../composeResources/values-uk/strings.xml | 30 +- .../composeResources/values-vi/strings.xml | 30 +- .../values-zh-rTW/strings.xml | 30 +- .../composeResources/values-zh/strings.xml | 30 +- .../composeResources/values/strings.xml | 47 +- .../org/spotube/spotube/core/di/Modules.kt | 8 +- .../org/spotube/spotube/core/jam/JamClock.kt | 118 + .../spotube/spotube/core/jam/JamProtocol.kt | 109 +- .../spotube/spotube/core/jam/JamRoomClient.kt | 663 +- .../spotube/spotube/core/jam/JamRoomCode.kt | 2 +- .../spotube/core/jam/JamRoomService.kt | 881 +- .../core/remote/RemotePlaybackController.kt | 2 +- .../spotube/spotube/modules/jam/JamScreen.kt | 8 +- .../spotube/modules/jam/JamViewModel.kt | 15 +- .../modules/settings/JamSettingsScreen.kt | 173 +- .../modules/settings/JamSettingsViewModel.kt | 9 +- .../modules/settings/SettingsModels.kt | 26 +- .../modules/settings/SettingsRepository.kt | 9 +- .../modules/settings/sections/JamSection.kt | 14 +- .../spotube/spotube/core/jam/JamSyncTest.kt | 135 + .../core/jam/JamRoomClientIntegrationTest.kt | 85 + .../core/jam/JamSessionSimulationTest.kt | 439 + .../org/spotube/spotube/core/jam/TcpProxy.kt | 96 + .../spotube/modules/jam/JamLayoutTest.kt | 20 +- gradle/libs.versions.toml | 6 - jam-server/.gitignore | 3 + jam-server/README.md | 72 + jam-server/package-lock.json | 1606 ++ jam-server/package.json | 22 + jam-server/src/index.ts | 44 + jam-server/src/protocol.ts | 85 + jam-server/src/room.ts | 311 + jam-server/test/smoke.mjs | 131 + jam-server/tsconfig.json | 14 + jam-server/worker-configuration.d.ts | 16269 ++++++++++++++++ jam-server/wrangler.jsonc | 15 + 57 files changed, 20727 insertions(+), 1399 deletions(-) create mode 100644 .github/workflows/jam-server-deploy.yml create mode 100644 composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt create mode 100644 composeApp/src/commonTest/kotlin/org/spotube/spotube/core/jam/JamSyncTest.kt create mode 100644 composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamRoomClientIntegrationTest.kt create mode 100644 composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamSessionSimulationTest.kt create mode 100644 composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/TcpProxy.kt create mode 100644 jam-server/.gitignore create mode 100644 jam-server/README.md create mode 100644 jam-server/package-lock.json create mode 100644 jam-server/package.json create mode 100644 jam-server/src/index.ts create mode 100644 jam-server/src/protocol.ts create mode 100644 jam-server/src/room.ts create mode 100644 jam-server/test/smoke.mjs create mode 100644 jam-server/tsconfig.json create mode 100644 jam-server/worker-configuration.d.ts create mode 100644 jam-server/wrangler.jsonc diff --git a/.github/workflows/jam-server-deploy.yml b/.github/workflows/jam-server-deploy.yml new file mode 100644 index 00000000..8ffe31b0 --- /dev/null +++ b/.github/workflows/jam-server-deploy.yml @@ -0,0 +1,35 @@ +name: Deploy Jam server + +on: + push: + branches: [master] + paths: ["jam-server/**"] + workflow_dispatch: + +concurrency: + group: jam-server-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + defaults: + run: + working-directory: jam-server + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: jam-server/package-lock.json + - run: npm ci + - run: npm run typecheck + - name: Deploy + run: npx wrangler deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + - name: Smoke test + run: JAM_URL="${{ vars.JAM_SERVER_URL }}" npm test + if: ${{ vars.JAM_SERVER_URL != '' }} diff --git a/AGENTS.md b/AGENTS.md index 816d9590..9d48dcfa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,14 @@ - Never call `HttpClient(...)` directly. Inject `HttpClientFactory` (`commonMain/.../core/network/HttpClientFactory.kt`, a Koin single) and use `.default` / `.json` (shared, never close them) or `.create { ... }` for extra plugins (caller owns and may close it). It owns the one engine-backed client, so the platform default engine is used everywhere (Darwin on iOS, OkHttp on Android/desktop). - Do not add `ktor-client-cio`: on iOS it competes with Darwin as the default engine and has no TLS on Kotlin/Native, so every HTTPS request fails. +## Group Jam +- Room server: `jam-server/` (Cloudflare Worker + one Durable Object per six-letter room code, TypeScript, wrangler). It relays/retains small JSON frames and serves the clock; no audio passes through it. Protocol in `jam-server/src/protocol.ts`; keep it in sync with the wire frames in `core/jam/JamRoomClient.kt` (bump `PROTOCOL_VERSION` on both sides for breaking changes). +- App side: `core/jam/` — `JamRoomClient` (Ktor WebSocket, reconnects, keep-alives, clock pings), `JamClock`/`JamDriftCorrector` (sync math), `JamRoomService` (host broadcasts queue + playback anchors in server time; guests follow by nudging playback rate, seeking only when far off). +- Cost and battery: the room is billed per incoming message and storage write, and on phones each transmission wakes the cellular radio. Don't add periodic frames or polling. Keep-alives go out once a minute. Guests' sync loop sleeps while the room is paused or buffering, and the host checks its own drift only while playing. Anchors go out on change (plus host drift and a 60 s heartbeat), keep-alives are runtime auto-responses, and anchors aren't persisted. See `jam-server/README.md`. +- A network drop must never crash the app: `JamRoomClient.pump` turns socket failures into a reconnect. `JamSessionSimulationTest.roomSurvivesNetworkLoss` cuts the network with `TcpProxy`. +- Server URL setting: `UserSettings.jamServerUrl` (default `DEFAULT_JAM_SERVER_URL` in `SettingsModels.kt`). +- Tests: `JamSyncTest` (pure), plus `JamRoomClientIntegrationTest` and `JamSessionSimulationTest`, which only run with `JAM_TEST_URL` set (start `npm run dev` in `jam-server/`). + ## GraalVM native image (desktop) - Enabled via `graalvm { }` inside `nucleus.application` in `composeApp/build.gradle.kts`. Nucleus downloads GraalVM CE itself (or uses `$GRAALVM_HOME`). Linux needs `gcc` + `patchelf`. - Build: `:composeApp:createGraalvmNativeDistributable` (output `composeApp/build/compose/binaries/main/graalvm-app/`), installers via `packageGraalvm{Deb,Rpm,AppImage,Dmg,Msi,Exe}`. A build takes ~10 min and uses all cores and ~15 GB RAM. diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f60ba709..38967b31 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -289,12 +289,6 @@ kotlin { // DLNA implementation(libs.dns.sd.kt) - // mqtt client for jam-session - implementation(libs.mqtt.client) - implementation(libs.mqtt.x.models) - implementation(libs.mqtt.buffer) - implementation(libs.mqtt.buffer.codec) - // Markdown renderer implementation(libs.multiplatform.markdown.renderer) implementation(libs.multiplatform.markdown.renderer.m3) @@ -443,14 +437,6 @@ android { } -// ditchoom's socket-quic-quiche ships libquiche_jni.so with 4 KB LOAD segment alignment -// (not 16 KB compatible), even in the latest release. It is only an optional QUIC transport -// pulled in transitively by mqtt-client, so drop it on Android until upstream fixes it. -configurations.matching { it.name.startsWith("debug") || it.name.startsWith("release") }.configureEach { - exclude(group = "com.ditchoom", module = "socket-quic-quiche") - exclude(group = "com.ditchoom", module = "socket-quic-quiche-android") -} - dependencies { coreLibraryDesugaring(libs.desugar.jdk.libs) debugImplementation(libs.compose.uiTooling) diff --git a/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt b/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt index 74df39e7..26f5a071 100644 --- a/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt +++ b/composeApp/src/androidMain/kotlin/org/spotube/spotube/media/PlaybackService.kt @@ -165,7 +165,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { librarySession.release() super.onDestroy() // Swiping the app out of recents should fully close it: Android otherwise keeps this - // process cached with the proxy server, MQTT client and every singleton still alive. + // process cached with the proxy server, jam client and every singleton still alive. if (taskRemoved) Process.killProcess(Process.myPid()) } diff --git a/composeApp/src/commonMain/composeResources/values-ar/strings.xml b/composeApp/src/commonMain/composeResources/values-ar/strings.xml index 9eb21d8c..7910c67d 100644 --- a/composeApp/src/commonMain/composeResources/values-ar/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ar/strings.xml @@ -308,25 +308,11 @@ WebView عرض محتوى الويب داخل التطبيق الجام الجماعي - وسيط MQTT - مضيف الوسيط - %1$s:%2$d - منفذ الوسيط - استخدام TLS - اسم المستخدم (اختياري) - كلمة المرور (اختياري) - بادئة معرّف العميل - تُستخدم لتعريف نسخة Spotube هذه - تشفير الاتصال بالوسيط - تحقق من إعدادات الوسيط الحالية قبل بدء غرفة. - اختبار الاتصال - جارٍ الاختبار… - تم الاتصال خلال %1$d ms - فشل: %1$s - وسيط مؤقت — اضبط وسيطك الخاص للاستضافة الذاتية + اختبار الاتصال + جارٍ الاختبار… + تم الاتصال خلال %1$d ms + فشل: %1$s الاتصال - بيانات الاعتماد - الهوية التشخيص @@ -339,9 +325,6 @@ جارٍ الاتصال… رمز الغرفة ABC123 - الوسيط: %1$s - اضبط وسيطًا في الإعدادات لاستضافة غرفة أو الانضمام إليها. - إعدادات الوسيط متصل جارٍ إعادة الاتصال… نسخ الرمز @@ -533,14 +516,9 @@ اقتراح للجام - لم يتم ضبط وسيط للجام - مضيف الوسيط فارغ فشل الاتصال انقطع الاتصال - - الوسيط - مجهول تمت الإضافة إلى قائمة انتظار الجام diff --git a/composeApp/src/commonMain/composeResources/values-bn/strings.xml b/composeApp/src/commonMain/composeResources/values-bn/strings.xml index 268cdf95..d5742676 100644 --- a/composeApp/src/commonMain/composeResources/values-bn/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-bn/strings.xml @@ -292,25 +292,11 @@ WebView অ্যাপের ভেতরে ওয়েব কনটেন্ট দেখানো গ্রুপ জ্যাম - MQTT ব্রোকার - ব্রোকার হোস্ট - %1$s:%2$d - ব্রোকার পোর্ট - TLS ব্যবহার করুন - ইউজারনেম (ঐচ্ছিক) - পাসওয়ার্ড (ঐচ্ছিক) - ক্লায়েন্ট ID প্রিফিক্স - এই Spotube শনাক্ত করতে ব্যবহৃত হয় - ব্রোকার কানেকশন এনক্রিপ্ট করুন - রুম শুরু করার আগে বর্তমান ব্রোকার সেটিংস যাচাই করুন। - কানেকশন পরীক্ষা করুন - পরীক্ষা চলছে… - %1$d ms-এ কানেক্ট হয়েছে - ব্যর্থ: %1$s - প্লেসহোল্ডার ব্রোকার — সেলফ-হোস্ট করতে নিজেরটি কনফিগার করুন + কানেকশন পরীক্ষা করুন + পরীক্ষা চলছে… + %1$d ms-এ কানেক্ট হয়েছে + ব্যর্থ: %1$s কানেকশন - ক্রেডেনশিয়াল - পরিচিতি ডায়াগনস্টিকস @@ -323,9 +309,6 @@ কানেক্ট হচ্ছে… রুম কোড ABC123 - ব্রোকার: %1$s - রুম হোস্ট করতে বা যোগ দিতে সেটিংসে একটি ব্রোকার সেট আপ করুন। - ব্রোকার সেটিংস কানেক্টেড আবার কানেক্ট হচ্ছে… কোড কপি করুন @@ -517,14 +500,9 @@ জ্যামে সাজেস্ট করুন - কোনো জ্যাম ব্রোকার কনফিগার করা নেই - ব্রোকার হোস্ট ফাঁকা কানেকশন ব্যর্থ হয়েছে কানেকশন বিচ্ছিন্ন হয়েছে - - ব্রোকার - anonymous জ্যাম কিউতে যোগ করা হয়েছে diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml index 2f965807..0837991c 100644 --- a/composeApp/src/commonMain/composeResources/values-de/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml @@ -292,25 +292,11 @@ WebView Webinhalte in der App anzeigen Gruppen-Jam - MQTT-Broker - Broker-Host - %1$s:%2$d - Broker-Port - TLS verwenden - Benutzername (optional) - Passwort (optional) - Client-ID-Präfix - Dient zur Identifizierung dieser Spotube-Instanz - Verbindung zum Broker verschlüsseln - Prüfe die aktuellen Broker-Einstellungen, bevor du einen Raum startest. - Verbindung testen - Wird getestet… - Verbunden in %1$d ms - Fehlgeschlagen: %1$s - Platzhalter-Broker – richte deinen eigenen ein, um selbst zu hosten + Verbindung testen + Wird getestet… + Verbunden in %1$d ms + Fehlgeschlagen: %1$s Verbindung - Zugangsdaten - Identität Diagnose @@ -323,9 +309,6 @@ Verbinden… Raumcode ABC123 - Broker: %1$s - Richte in den Einstellungen einen Broker ein, um einen Raum zu hosten oder beizutreten. - Broker-Einstellungen Verbunden Verbindung wird wiederhergestellt… Code kopieren @@ -517,14 +500,9 @@ Für Jam vorschlagen - Kein Jam-Broker konfiguriert - Broker-Host ist leer Verbindung fehlgeschlagen Verbindung unterbrochen - - Broker - anonym Zur Jam-Warteschlange hinzugefügt diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml index b4ae5fa1..6d486799 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml @@ -296,25 +296,11 @@ WebView Mostrar contenido web dentro de la app Jam en grupo - Broker MQTT - Host del broker - %1$s:%2$d - Puerto del broker - Usar TLS - Usuario (opcional) - Contraseña (opcional) - Prefijo de ID de cliente - Se usa para identificar esta instancia de Spotube - Cifra la conexión con el broker - Comprueba los ajustes actuales del broker antes de crear una sala. - Probar conexión - Probando… - Conectado en %1$d ms - Error: %1$s - Broker de ejemplo: configura el tuyo para alojarlo tú mismo + Probar conexión + Probando… + Conectado en %1$d ms + Error: %1$s Conexión - Credenciales - Identidad Diagnóstico @@ -327,9 +313,6 @@ Conectando… Código de la sala ABC123 - Broker: %1$s - Configura un broker en Ajustes para crear una sala o unirte a una. - Ajustes del broker Conectado Reconectando… Copiar código @@ -521,14 +504,9 @@ Sugerir al Jam - No hay ningún broker de Jam configurado - El host del broker está vacío Error de conexión Se perdió la conexión - - Broker - anónimo Añadida a la cola del Jam diff --git a/composeApp/src/commonMain/composeResources/values-fa/strings.xml b/composeApp/src/commonMain/composeResources/values-fa/strings.xml index 51374e7e..3c0dbc1a 100644 --- a/composeApp/src/commonMain/composeResources/values-fa/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fa/strings.xml @@ -292,25 +292,11 @@ WebView نمایش محتوای وب درون برنامه جم گروهی - بروکر MQTT - میزبان بروکر - %1$s:%2$d - درگاه بروکر - استفاده از TLS - نام کاربری (اختیاری) - گذرواژه (اختیاری) - پیشوند شناسهٔ کلاینت - برای شناسایی این نسخه از Spotube استفاده می‌شود - رمزگذاری اتصال به بروکر - پیش از شروع اتاق، تنظیمات فعلی بروکر را بررسی کنید. - آزمایش اتصال - در حال آزمایش… - در %1$d میلی‌ثانیه متصل شد - ناموفق: %1$s - بروکر نمونه — برای میزبانی شخصی، بروکر خودتان را پیکربندی کنید + آزمایش اتصال + در حال آزمایش… + در %1$d میلی‌ثانیه متصل شد + ناموفق: %1$s اتصال - اطلاعات ورود - هویت عیب‌یابی @@ -323,9 +309,6 @@ در حال اتصال… کد اتاق ABC123 - بروکر: %1$s - برای میزبانی یا پیوستن به اتاق، یک بروکر در تنظیمات راه‌اندازی کنید. - تنظیمات بروکر متصل در حال اتصال دوباره… کپی کد @@ -517,14 +500,9 @@ پیشنهاد به جم - هیچ بروکر جمی پیکربندی نشده است - میزبان بروکر خالی است اتصال ناموفق بود اتصال قطع شد - - بروکر - ناشناس به صف جم اضافه شد diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index 45a31bd6..4372e030 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -296,25 +296,11 @@ WebView Afficher du contenu web dans l\'application Jam de groupe - Broker MQTT - Hôte du broker - %1$s:%2$d - Port du broker - Utiliser TLS - Nom d\'utilisateur (facultatif) - Mot de passe (facultatif) - Préfixe de l\'ID client - Sert à identifier cette instance de Spotube - Chiffrer la connexion au broker - Vérifiez les paramètres actuels du broker avant de créer une salle. - Tester la connexion - Test en cours… - Connecté en %1$d ms - Échec : %1$s - Broker temporaire : configurez le vôtre pour l\'auto-héberger + Tester la connexion + Test en cours… + Connecté en %1$d ms + Échec : %1$s Connexion - Identifiants - Identité Diagnostic @@ -327,9 +313,6 @@ Connexion… Code de la salle ABC123 - Broker : %1$s - Configurez un broker dans les Paramètres pour créer ou rejoindre une salle. - Paramètres du broker Connecté Reconnexion… Copier le code @@ -521,14 +504,9 @@ Proposer à la Jam - Aucun broker Jam configuré - L\'hôte du broker est vide Échec de la connexion Connexion perdue - - Broker - anonyme Ajouté à la file d\'attente de la Jam diff --git a/composeApp/src/commonMain/composeResources/values-hi/strings.xml b/composeApp/src/commonMain/composeResources/values-hi/strings.xml index 29e2b1d2..50c52c2d 100644 --- a/composeApp/src/commonMain/composeResources/values-hi/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-hi/strings.xml @@ -291,25 +291,11 @@ WebView ऐप के अंदर वेब कंटेंट दिखाएं ग्रुप जैम - MQTT ब्रोकर - ब्रोकर होस्ट - %1$s:%2$d - ब्रोकर पोर्ट - TLS इस्तेमाल करें - यूज़रनेम (वैकल्पिक) - पासवर्ड (वैकल्पिक) - क्लाइंट ID प्रीफ़िक्स - इस Spotube की पहचान के लिए इस्तेमाल होता है - ब्रोकर कनेक्शन एन्क्रिप्ट करें - रूम शुरू करने से पहले मौजूदा ब्रोकर सेटिंग जांचें। - कनेक्शन टेस्ट करें - टेस्ट हो रहा है… - %1$d ms में कनेक्ट हुआ - विफल: %1$s - प्लेसहोल्डर ब्रोकर — खुद होस्ट करने के लिए अपना ब्रोकर सेट करें + कनेक्शन टेस्ट करें + टेस्ट हो रहा है… + %1$d ms में कनेक्ट हुआ + विफल: %1$s कनेक्शन - क्रेडेंशियल - पहचान डायग्नोस्टिक्स @@ -322,9 +308,6 @@ कनेक्ट हो रहा है… रूम कोड ABC123 - ब्रोकर: %1$s - रूम होस्ट करने या उससे जुड़ने के लिए सेटिंग में ब्रोकर सेट करें। - ब्रोकर सेटिंग कनेक्ट है फिर से कनेक्ट हो रहा है… कोड कॉपी करें @@ -516,14 +499,9 @@ जैम में सुझाएं - कोई जैम ब्रोकर सेट नहीं है - ब्रोकर होस्ट खाली है कनेक्शन विफल कनेक्शन टूट गया - - ब्रोकर - anonymous जैम कतार में जोड़ा गया diff --git a/composeApp/src/commonMain/composeResources/values-id/strings.xml b/composeApp/src/commonMain/composeResources/values-id/strings.xml index 3f46bc82..1b8ee7e5 100644 --- a/composeApp/src/commonMain/composeResources/values-id/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-id/strings.xml @@ -288,25 +288,11 @@ WebView Menampilkan konten web di dalam aplikasi Jam Grup - Broker MQTT - Host broker - %1$s:%2$d - Port broker - Gunakan TLS - Nama pengguna (opsional) - Kata sandi (opsional) - Prefiks ID klien - Dipakai untuk mengidentifikasi Spotube ini - Enkripsi koneksi broker - Periksa pengaturan broker saat ini sebelum memulai ruang. - Uji koneksi - Menguji… - Terhubung dalam %1$d ms - Gagal: %1$s - Broker sementara — konfigurasikan milikmu sendiri untuk self-host + Uji koneksi + Menguji… + Terhubung dalam %1$d ms + Gagal: %1$s Koneksi - Kredensial - Identitas Diagnostik @@ -319,9 +305,6 @@ Menghubungkan… Kode ruang ABC123 - Broker: %1$s - Atur broker di Pengaturan untuk membuat atau bergabung ke ruang. - Pengaturan broker Terhubung Menghubungkan ulang… Salin kode @@ -513,14 +496,9 @@ Sarankan ke Jam - Belum ada broker Jam yang dikonfigurasi - Host broker kosong Koneksi gagal Koneksi terputus - - Broker - anonim Ditambahkan ke antrean Jam diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml index 2d956a2c..2a71283c 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml @@ -296,25 +296,11 @@ WebView Mostrare contenuti web all\'interno dell\'app Jam di gruppo - Broker MQTT - Host del broker - %1$s:%2$d - Porta del broker - Usa TLS - Nome utente (facoltativo) - Password (facoltativa) - Prefisso ID client - Usato per identificare questa istanza di Spotube - Crittografa la connessione al broker - Verifica le impostazioni attuali del broker prima di avviare una stanza. - Verifica connessione - Verifica in corso… - Connesso in %1$d ms - Non riuscito: %1$s - Broker di esempio: configurane uno tuo per l\'hosting autonomo + Verifica connessione + Verifica in corso… + Connesso in %1$d ms + Non riuscito: %1$s Connessione - Credenziali - Identità Diagnostica @@ -327,9 +313,6 @@ Connessione… Codice stanza ABC123 - Broker: %1$s - Configura un broker nelle Impostazioni per ospitare una stanza o parteciparvi. - Impostazioni broker Connesso Riconnessione… Copia codice @@ -521,14 +504,9 @@ Suggerisci alla Jam - Nessun broker Jam configurato - L\'host del broker è vuoto Connessione non riuscita Connessione persa - - Broker - anonimo Aggiunto alla coda della Jam diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml index 27ea528a..473e213d 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml @@ -288,25 +288,11 @@ WebView アプリ内でのWebコンテンツの表示 グループジャム - MQTTブローカー - ブローカーのホスト - %1$s:%2$d - ブローカーのポート - TLSを使用 - ユーザー名 (任意) - パスワード (任意) - クライアントIDのプレフィックス - このSpotubeを識別するために使用されます - ブローカーとの接続を暗号化します - ルームを開始する前に、現在のブローカー設定を確認します。 - 接続をテスト - テスト中… - %1$d msで接続しました - 失敗: %1$s - 仮のブローカーです。セルフホストするには独自のブローカーを設定してください + 接続をテスト + テスト中… + %1$d msで接続しました + 失敗: %1$s 接続 - 認証情報 - 識別情報 診断 @@ -319,9 +305,6 @@ 接続中… ルームコード ABC123 - ブローカー: %1$s - ルームを主催または参加するには、設定でブローカーを設定してください。 - ブローカー設定 接続済み 再接続中… コードをコピー @@ -513,14 +496,9 @@ ジャムに提案 - ジャム用ブローカーが設定されていません - ブローカーのホストが空です 接続に失敗しました 接続が切断されました - - ブローカー - anonymous ジャムのキューに追加しました diff --git a/composeApp/src/commonMain/composeResources/values-ko/strings.xml b/composeApp/src/commonMain/composeResources/values-ko/strings.xml index 67fb729e..69dac887 100644 --- a/composeApp/src/commonMain/composeResources/values-ko/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ko/strings.xml @@ -288,25 +288,11 @@ WebView 앱 안에 웹 콘텐츠 표시 그룹 잼 - MQTT 브로커 - 브로커 호스트 - %1$s:%2$d - 브로커 포트 - TLS 사용 - 사용자 이름(선택 사항) - 비밀번호(선택 사항) - 클라이언트 ID 접두사 - 이 Spotube 인스턴스를 식별하는 데 사용돼요 - 브로커 연결 암호화 - 방을 시작하기 전에 현재 브로커 설정을 확인하세요. - 연결 테스트 - 테스트 중… - %1$d ms 만에 연결됨 - 실패: %1$s - 임시 브로커입니다 — 직접 호스팅하려면 브로커를 설정하세요 + 연결 테스트 + 테스트 중… + %1$d ms 만에 연결됨 + 실패: %1$s 연결 - 인증 정보 - 식별 정보 진단 @@ -319,9 +305,6 @@ 연결 중… 방 코드 ABC123 - 브로커: %1$s - 방을 만들거나 참여하려면 설정에서 브로커를 설정하세요. - 브로커 설정 연결됨 다시 연결 중… 코드 복사 @@ -513,14 +496,9 @@ 잼에 추천하기 - 구성된 잼 브로커가 없어요 - 브로커 호스트가 비어 있어요 연결 실패 연결 끊김 - - 브로커 - 익명 잼 대기열에 추가했어요 diff --git a/composeApp/src/commonMain/composeResources/values-nl/strings.xml b/composeApp/src/commonMain/composeResources/values-nl/strings.xml index a5e4197e..c1b6c506 100644 --- a/composeApp/src/commonMain/composeResources/values-nl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-nl/strings.xml @@ -292,25 +292,11 @@ WebView Webcontent weergeven in de app Group Jam - MQTT-broker - Broker-host - %1$s:%2$d - Broker-poort - TLS gebruiken - Gebruikersnaam (optioneel) - Wachtwoord (optioneel) - Voorvoegsel client-ID - Wordt gebruikt om deze Spotube-installatie te herkennen - De verbinding met de broker versleutelen - Controleer de huidige broker-instellingen voordat je een Jam start. - Verbinding testen - Testen… - Verbonden in %1$d ms - Mislukt: %1$s - Tijdelijke broker — stel je eigen broker in om zelf te hosten + Verbinding testen + Testen… + Verbonden in %1$d ms + Mislukt: %1$s Verbinding - Inloggegevens - Identiteit Diagnose @@ -323,9 +309,6 @@ Verbinden… Sessiecode ABC123 - Broker: %1$s - Stel een broker in bij Instellingen om een sessie te hosten of eraan deel te nemen. - Broker-instellingen Verbonden Opnieuw verbinden… Code kopiëren @@ -517,14 +500,9 @@ Voorstellen aan Jam - Geen Jam-broker ingesteld - Broker-host is leeg Verbinding mislukt Verbinding verbroken - - Broker - anoniem Toegevoegd aan de Jam-wachtrij diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml index 5af41a10..b749cd80 100644 --- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml @@ -300,25 +300,11 @@ WebView Wyświetlanie treści internetowych w aplikacji Grupowy Jam - Broker MQTT - Host brokera - %1$s:%2$d - Port brokera - Użyj TLS - Nazwa użytkownika (opcjonalnie) - Hasło (opcjonalnie) - Prefiks ID klienta - Służy do identyfikacji tej instancji Spotube - Szyfruj połączenie z brokerem - Sprawdź bieżące ustawienia brokera przed utworzeniem pokoju. - Testuj połączenie - Testowanie… - Połączono w %1$d ms - Niepowodzenie: %1$s - Broker zastępczy – skonfiguruj własny, aby hostować samodzielnie + Testuj połączenie + Testowanie… + Połączono w %1$d ms + Niepowodzenie: %1$s Połączenie - Dane logowania - Tożsamość Diagnostyka @@ -331,9 +317,6 @@ Łączenie… Kod pokoju ABC123 - Broker: %1$s - Skonfiguruj brokera w Ustawieniach, aby utworzyć pokój lub do niego dołączyć. - Ustawienia brokera Połączono Ponowne łączenie… Kopiuj kod @@ -525,14 +508,9 @@ Zaproponuj w Jamie - Nie skonfigurowano brokera Jam - Host brokera jest pusty Połączenie nieudane Utracono połączenie - - Broker - anonim Dodano do kolejki Jamu diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml index ce987e2b..c1fdef3b 100644 --- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml @@ -296,25 +296,11 @@ WebView Exibir conteúdo da web dentro do app Jam em grupo - Broker MQTT - Host do broker - %1$s:%2$d - Porta do broker - Usar TLS - Nome de usuário (opcional) - Senha (opcional) - Prefixo do ID do cliente - Usado para identificar esta instância do Spotube - Criptografa a conexão com o broker - Verifique as configurações atuais do broker antes de criar uma sala. - Testar conexão - Testando… - Conectado em %1$d ms - Falhou: %1$s - Broker provisório — configure o seu próprio para hospedar por conta própria + Testar conexão + Testando… + Conectado em %1$d ms + Falhou: %1$s Conexão - Credenciais - Identidade Diagnóstico @@ -327,9 +313,6 @@ Conectando… Código da sala ABC123 - Broker: %1$s - Configure um broker nas Configurações para criar ou entrar em uma sala. - Configurações do broker Conectado Reconectando… Copiar código @@ -521,14 +504,9 @@ Sugerir na Jam - Nenhum broker de Jam configurado - O host do broker está vazio Falha na conexão Conexão perdida - - Broker - anônimo Adicionada à fila da Jam diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings.xml b/composeApp/src/commonMain/composeResources/values-ru/strings.xml index 8128a8ce..eaa76c30 100644 --- a/composeApp/src/commonMain/composeResources/values-ru/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ru/strings.xml @@ -300,25 +300,11 @@ WebView Показ веб-контента внутри приложения Групповой джем - MQTT-брокер - Хост брокера - %1$s:%2$d - Порт брокера - Использовать TLS - Имя пользователя (необязательно) - Пароль (необязательно) - Префикс ID клиента - Используется для идентификации этого экземпляра Spotube - Шифровать подключение к брокеру - Проверьте текущие настройки брокера перед созданием комнаты. - Проверить подключение - Проверка… - Подключено за %1$d мс - Ошибка: %1$s - Временный брокер — настройте свой, чтобы размещать джемы самостоятельно + Проверить подключение + Проверка… + Подключено за %1$d мс + Ошибка: %1$s Подключение - Учётные данные - Идентификация Диагностика @@ -331,9 +317,6 @@ Подключение… Код комнаты ABC123 - Брокер: %1$s - Настройте брокер в настройках, чтобы создать комнату или присоединиться к ней. - Настройки брокера Подключено Переподключение… Копировать код @@ -525,14 +508,9 @@ Предложить в джем - Брокер для джема не настроен - Не указан хост брокера Не удалось подключиться Соединение потеряно - - Брокер - anonymous Добавлено в очередь джема diff --git a/composeApp/src/commonMain/composeResources/values-th/strings.xml b/composeApp/src/commonMain/composeResources/values-th/strings.xml index ab470a92..56a198a2 100644 --- a/composeApp/src/commonMain/composeResources/values-th/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-th/strings.xml @@ -288,25 +288,11 @@ WebView แสดงเนื้อหาเว็บภายในแอป แจมกลุ่ม - MQTT Broker - โฮสต์ของ Broker - %1$s:%2$d - พอร์ตของ Broker - ใช้ TLS - ชื่อผู้ใช้ (ไม่บังคับ) - รหัสผ่าน (ไม่บังคับ) - คำนำหน้า Client ID - ใช้เพื่อระบุ Spotube เครื่องนี้ - เข้ารหัสการเชื่อมต่อกับ Broker - ตรวจสอบการตั้งค่า Broker ปัจจุบันก่อนเริ่มห้อง - ทดสอบการเชื่อมต่อ - กำลังทดสอบ… - เชื่อมต่อแล้วใน %1$d ms - ล้มเหลว: %1$s - Broker ตัวอย่าง — กำหนดค่าของคุณเองเพื่อโฮสต์เอง + ทดสอบการเชื่อมต่อ + กำลังทดสอบ… + เชื่อมต่อแล้วใน %1$d ms + ล้มเหลว: %1$s การเชื่อมต่อ - ข้อมูลเข้าสู่ระบบ - ตัวตน การวินิจฉัย @@ -319,9 +305,6 @@ กำลังเชื่อมต่อ… รหัสห้อง ABC123 - Broker: %1$s - ตั้งค่า Broker ในการตั้งค่าเพื่อสร้างหรือเข้าร่วมห้อง - การตั้งค่า Broker เชื่อมต่อแล้ว กำลังเชื่อมต่อใหม่… คัดลอกรหัส @@ -513,14 +496,9 @@ เสนอให้แจม - ยังไม่ได้กำหนดค่า Broker สำหรับแจม - โฮสต์ของ Broker ว่างอยู่ การเชื่อมต่อล้มเหลว การเชื่อมต่อขาดหาย - - Broker - anonymous เพิ่มลงในคิวแจมแล้ว diff --git a/composeApp/src/commonMain/composeResources/values-tr/strings.xml b/composeApp/src/commonMain/composeResources/values-tr/strings.xml index 54d5ed93..2f8c21e9 100644 --- a/composeApp/src/commonMain/composeResources/values-tr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-tr/strings.xml @@ -292,25 +292,11 @@ WebView Uygulama içinde web içeriği görüntüleme Grup Jam - MQTT Aracısı - Aracı ana bilgisayarı - %1$s:%2$d - Aracı portu - TLS kullan - Kullanıcı adı (isteğe bağlı) - Şifre (isteğe bağlı) - İstemci kimliği ön eki - Bu Spotube örneğini tanımlamak için kullanılır - Aracı bağlantısını şifrele - Oda başlatmadan önce geçerli aracı ayarlarını kontrol et. - Bağlantıyı test et - Test ediliyor… - %1$d ms\'de bağlandı - Başarısız: %1$s - Geçici aracı — kendin barındırmak için kendi aracını yapılandır + Bağlantıyı test et + Test ediliyor… + %1$d ms\'de bağlandı + Başarısız: %1$s Bağlantı - Kimlik bilgileri - Kimlik Tanılama @@ -323,9 +309,6 @@ Bağlanıyor… Oda kodu ABC123 - Aracı: %1$s - Oda açmak veya bir odaya katılmak için Ayarlar\'da bir aracı ayarla. - Aracı ayarları Bağlandı Yeniden bağlanıyor… Kodu kopyala @@ -517,14 +500,9 @@ Jam\'e öner - Jam aracısı yapılandırılmadı - Aracı ana bilgisayarı boş Bağlantı başarısız Bağlantı kesildi - - Aracı - anonymous Jam sırasına eklendi diff --git a/composeApp/src/commonMain/composeResources/values-uk/strings.xml b/composeApp/src/commonMain/composeResources/values-uk/strings.xml index 0fd2d19b..9f87d27f 100644 --- a/composeApp/src/commonMain/composeResources/values-uk/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-uk/strings.xml @@ -300,25 +300,11 @@ WebView Показ вебвмісту всередині застосунку Груповий джем - MQTT-брокер - Хост брокера - %1$s:%2$d - Порт брокера - Використовувати TLS - Ім’я користувача (необов’язково) - Пароль (необов’язково) - Префікс ID клієнта - Використовується для ідентифікації цього екземпляра Spotube - Шифрувати підключення до брокера - Перевірте поточні налаштування брокера перед створенням кімнати. - Перевірити підключення - Перевірка… - Підключено за %1$d мс - Помилка: %1$s - Тимчасовий брокер — налаштуйте власний, щоб розміщувати джеми самостійно + Перевірити підключення + Перевірка… + Підключено за %1$d мс + Помилка: %1$s Підключення - Облікові дані - Ідентифікація Діагностика @@ -331,9 +317,6 @@ Підключення… Код кімнати ABC123 - Брокер: %1$s - Налаштуйте брокер у налаштуваннях, щоб створити кімнату або приєднатися до неї. - Налаштування брокера Підключено Повторне підключення… Копіювати код @@ -525,14 +508,9 @@ Запропонувати в джем - Брокер для джему не налаштовано - Не вказано хост брокера Не вдалося підключитися З’єднання втрачено - - Брокер - anonymous Додано до черги джему diff --git a/composeApp/src/commonMain/composeResources/values-vi/strings.xml b/composeApp/src/commonMain/composeResources/values-vi/strings.xml index 39ae5edc..80a93679 100644 --- a/composeApp/src/commonMain/composeResources/values-vi/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-vi/strings.xml @@ -288,25 +288,11 @@ WebView Hiển thị nội dung web trong ứng dụng Jam nhóm - MQTT Broker - Máy chủ broker - %1$s:%2$d - Cổng broker - Dùng TLS - Tên người dùng (không bắt buộc) - Mật khẩu (không bắt buộc) - Tiền tố Client ID - Dùng để nhận diện Spotube này - Mã hóa kết nối tới broker - Kiểm tra cài đặt broker hiện tại trước khi tạo phòng. - Kiểm tra kết nối - Đang kiểm tra… - Đã kết nối trong %1$d ms - Thất bại: %1$s - Broker tạm thời — hãy cấu hình broker riêng để tự lưu trữ + Kiểm tra kết nối + Đang kiểm tra… + Đã kết nối trong %1$d ms + Thất bại: %1$s Kết nối - Thông tin đăng nhập - Danh tính Chẩn đoán @@ -319,9 +305,6 @@ Đang kết nối… Mã phòng ABC123 - Broker: %1$s - Hãy thiết lập broker trong Cài đặt để tạo hoặc tham gia phòng. - Cài đặt broker Đã kết nối Đang kết nối lại… Sao chép mã @@ -513,14 +496,9 @@ Đề xuất cho Jam - Chưa cấu hình broker cho Jam - Máy chủ broker đang trống Kết nối thất bại Mất kết nối - - Broker - ẩn danh Đã thêm vào danh sách chờ của Jam diff --git a/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml b/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml index 2f59a04d..f3710bb5 100644 --- a/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -288,25 +288,11 @@ WebView 在應用程式內顯示網頁內容 群組 Jam - MQTT 代理伺服器 - 代理伺服器主機 - %1$s:%2$d - 代理伺服器連接埠 - 使用 TLS - 使用者名稱(選填) - 密碼(選填) - 用戶端 ID 前綴 - 用於識別這個 Spotube - 加密與代理伺服器的連線 - 在建立房間前檢查目前的代理伺服器設定。 - 測試連線 - 測試中… - 已連線,耗時 %1$d 毫秒 - 失敗:%1$s - 預留的代理伺服器 — 請設定你自己的伺服器以自行架設 + 測試連線 + 測試中… + 已連線,耗時 %1$d 毫秒 + 失敗:%1$s 連線 - 憑證 - 身分 診斷 @@ -319,9 +305,6 @@ 連線中… 房間代碼 ABC123 - 代理伺服器:%1$s - 請先在「設定」中設定代理伺服器,才能建立或加入房間。 - 代理伺服器設定 已連線 重新連線中… 複製代碼 @@ -513,14 +496,9 @@ 推薦到 Jam - 尚未設定 Jam 代理伺服器 - 代理伺服器主機為空白 連線失敗 連線中斷 - - 代理伺服器 - anonymous 已加入 Jam 待播清單 diff --git a/composeApp/src/commonMain/composeResources/values-zh/strings.xml b/composeApp/src/commonMain/composeResources/values-zh/strings.xml index 774ab6ef..722ff10c 100644 --- a/composeApp/src/commonMain/composeResources/values-zh/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-zh/strings.xml @@ -288,25 +288,11 @@ WebView 在应用内显示网页内容 一起听 - MQTT 代理服务器 - 代理服务器主机 - %1$s:%2$d - 代理服务器端口 - 使用 TLS - 用户名(可选) - 密码(可选) - 客户端 ID 前缀 - 用于识别此 Spotube 实例 - 加密与代理服务器的连接 - 创建房间前,检查当前的代理服务器设置。 - 测试连接 - 正在测试… - 已连接,耗时 %1$d 毫秒 - 失败:%1$s - 占位代理服务器 — 如需自托管,请配置你自己的服务器 + 测试连接 + 正在测试… + 已连接,耗时 %1$d 毫秒 + 失败:%1$s 连接 - 凭据 - 身份 诊断 @@ -319,9 +305,6 @@ 正在连接… 房间码 ABC123 - 代理服务器:%1$s - 请先在设置中配置代理服务器,才能创建或加入房间。 - 代理服务器设置 已连接 正在重新连接… 复制房间码 @@ -513,14 +496,9 @@ 推荐到一起听 - 未配置一起听代理服务器 - 代理服务器主机为空 连接失败 连接已断开 - - 代理服务器 - anonymous 已添加到一起听队列 diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 2fbae5e0..764bff82 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -310,25 +310,16 @@ WebView Display web content inside the app Group Jam - MQTT Broker - Broker host - %1$s:%2$d - Broker port - Use TLS - Username (optional) - Password (optional) - Client ID prefix - Used to identify this Spotube instance - Encrypt the broker connection - Check the current broker settings before starting a room. - Test connection - Testing… - Connected in %1$d ms - Failed: %1$s - Placeholder broker — configure your own to self-host + Room server + Server URL + Enter a URL starting with https:// + Check the room server before starting a room. + Jam uses Spotube’s free room server. To self-host, deploy jam-server from the Spotube repository and enter its URL. + Test connection + Testing… + Connected in %1$d ms + Failed: %1$s Connection - Credentials - Identity Diagnostics @@ -341,9 +332,9 @@ Connecting… Room code ABC123 - Broker: %1$s - Set up a broker in Settings to host or join a room. - Broker settings + Server: %1$s + Set a room server in Settings to host or join a room. + Server settings Connected Reconnecting… Copy code @@ -549,15 +540,17 @@ Suggest to Jam - No jam broker configured - Broker host is empty + No jam server configured + Couldn’t reach the jam server. Check your network or the server URL in Settings → Jam. + The jam server runs an incompatible version. + No room with that code. Check it and try again. + This room is full. + The host banned you from this room. + The host removed you from the room. + You joined this room from another device. Connection failed Connection lost - Couldn't reach %1$s:%2$d. Check your network or pick another broker in Settings → Jam. - - Broker - anonymous Added to the jam queue diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt index d7668946..4c45a3fd 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/di/Modules.kt @@ -86,6 +86,7 @@ import org.spotube.spotube.modules.update.AppUpdateService import org.spotube.spotube.modules.update.PluginUpdateService import org.spotube.spotube.modules.settings.SettingsProvider import org.spotube.spotube.modules.settings.SettingsRepository +import org.spotube.spotube.modules.settings.UserSettingsStore import org.spotube.spotube.modules.settings.UserSettingsSource import org.spotube.spotube.modules.settings.SettingsViewModel import org.spotube.spotube.modules.settings.JamSettingsViewModel @@ -151,7 +152,10 @@ val sharedModules = module { viewModelOf(::MetadataPluginGuardViewModel) // Settings - singleOf(::SettingsRepository) { bind() } + singleOf(::SettingsRepository) { + bind() + bind() + } viewModelOf(::SettingsViewModel) { bind() } viewModelOf(::JamSettingsViewModel) viewModel { @@ -283,7 +287,7 @@ val sharedModules = module { } single { RemotePlaybackController(get(), get(), get(), get(), get()) } singleOf(::JamRoomClient) - single { JamRoomService(get(), get(), get(), get()) } + single { JamRoomService(get(), get(), get(), get(), get(), get(), get()) } singleOf(::AudioPlayerQueueRepository) { bind() } single { DeviceAudioPlayerQueue(get(), get(), get(), get(), get()) diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt new file mode 100644 index 00000000..770a6834 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamClock.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.spotube.spotube.core.jam + +/** + * Estimates the offset between this device's clock and the room server's (NTP-style). + * + * Each ping is sent at local time `c` and answered with server time `s`; it arrives back + * at local time `r`. Assuming symmetric latency, the server read its clock halfway + * through, so `offset = s - (c + r) / 2`. Samples with the lowest round trip have the + * least room for asymmetry, so the estimate uses the fastest of the recent samples. + * Samples older than [maxAgeMs] are dropped, so the estimate follows slow clock drift. + */ +class JamClock(private val window: Int = 16, private val maxAgeMs: Long = 15 * 60_000L) { + private data class Sample(val rttMs: Long, val offsetMs: Long, val atMs: Long) + + private val samples = ArrayDeque() + + /** Local-to-server offset in ms, or null before the first sample. */ + var offsetMs: Long? = null + private set + + /** Round trip of the sample the offset came from. */ + var rttMs: Long? = null + private set + + fun addSample(clientSentMs: Long, serverMs: Long, clientReceivedMs: Long) { + val rtt = clientReceivedMs - clientSentMs + if (rtt < 0) return + samples.addLast(Sample(rtt, serverMs - (clientSentMs + clientReceivedMs) / 2, clientReceivedMs)) + while (samples.size > window || samples.first().atMs < clientReceivedMs - maxAgeMs) samples.removeFirst() + val best = samples.minBy { it.rttMs } + offsetMs = best.offsetMs + rttMs = best.rttMs + } + + fun serverTime(localMs: Long): Long = localMs + (offsetMs ?: 0L) + + fun reset() { + samples.clear() + offsetMs = null + rttMs = null + } +} + +/** What a guest does about the gap between where the room is and where it is. */ +sealed class JamCorrection { + /** In sync: play at the room's rate. */ + data object None : JamCorrection() + + /** Slightly off: speed up or slow down until caught up (inaudible, no gap). */ + data class Nudge(val rate: Float) : JamCorrection() + + /** Too far off to nudge: jump to [positionMs]. */ + data class Seek(val positionMs: Long) : JamCorrection() +} + +/** + * Turns drift into a [JamCorrection]. Players report their position coarsely (VLC in + * ~250 ms steps), so decisions use the median of the last few drift readings, and + * small drift inside [deadBandMs] is left alone. + */ +class JamDriftCorrector( + private val deadBandMs: Long = 120, + private val seekThresholdMs: Long = 1_500, + /** Drift is closed over roughly this long while nudging. */ + private val catchUpMs: Long = 3_000, + private val maxNudge: Float = 0.05f, + private val window: Int = 5, + /** Readings needed before a seek, so one noisy report right after a change can't cause one. */ + private val minReadingsToSeek: Int = 3) { + private val drifts = ArrayDeque() + + /** + * [expectedMs] is the room's position, [actualMs] ours, [roomRate] the host's rate. + * [seekLeadMs] is how long a seek takes to resume here; a seek aims that far ahead. + */ + fun correct(expectedMs: Long, actualMs: Long, roomRate: Float, seekLeadMs: Long): JamCorrection { + val drift = expectedMs - actualMs + // A huge jump is acted on at once; waiting for the median would replay it for seconds. + if (kotlin.math.abs(drift) > seekThresholdMs * 2) { + reset() + return JamCorrection.Seek(expectedMs + seekLeadMs) + } + drifts.addLast(drift) + while (drifts.size > window) drifts.removeFirst() + val smoothed = drifts.sorted()[drifts.size / 2] + + return when { + kotlin.math.abs(smoothed) > seekThresholdMs && drifts.size >= minReadingsToSeek -> { + reset() + JamCorrection.Seek(expectedMs + seekLeadMs) + } + kotlin.math.abs(smoothed) <= deadBandMs -> JamCorrection.None + else -> { + val nudge = (smoothed.toFloat() / catchUpMs).coerceIn(-maxNudge, maxNudge) + JamCorrection.Nudge(roomRate * (1f + nudge)) + } + } + } + + fun reset() = drifts.clear() +} diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt index 03231e66..b01fe8e0 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamProtocol.kt @@ -20,91 +20,73 @@ package org.spotube.spotube.core.jam import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import org.spotube.spotube.core.audioplayer.MediaItem import org.spotube.spotube.core.audioplayer.QueueEntry +import org.spotube.spotube.core.server.PluginTaggedAudioSource import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** - * Jam messages exchanged over MQTT. + * Room payloads. The server only routes and retains them; their shape is owned here. * - * - `state` topic: [QueueState] (retained, host -> everyone) - * - `cmd` topic: [PlaybackCommand], [Kick], [SuggestTrack], [SuggestPlaylist] - * (anyone -> host, except Kick which is host -> guest) - * - `presence/{clientId}` topic: [JamPresence] (retained, one per participant) + * - [JamQueue] (retained, host -> everyone): the shared queue. + * - [JamAnchor] (retained, host -> everyone): where playback is, in server time. + * - [JamCommand] (guest -> host): playback control and suggestions. */ @Serializable -sealed class JamMessage { - @Serializable - @SerialName("queueState") - data class QueueState( - val items: List, - val currentIndex: Int, - val shuffleEnabled: Boolean = false, - /** - * Whether guests should follow the host's current index. True when the - * host manually skipped/jumped or loaded a queue; false when the host - * merely auto-advanced because its song ended (guests stay put). - */ - val follow: Boolean = false, - /** Room-wide play/pause; guests follow every change of it. */ - val isPlaying: Boolean = false) : JamMessage() - - @Serializable - @SerialName("playbackCommand") - data class PlaybackCommand(val command: PlaybackCmd) : JamMessage() - - @Serializable - @SerialName("suggestTrack") - data class SuggestTrack( - val mediaItem: JamMediaItem, - val addedBy: String = "") : JamMessage() - - @Serializable - @SerialName("suggestPlaylist") - data class SuggestPlaylist( - val tracks: List, - val addedBy: String = "") : JamMessage() - - @Serializable - @SerialName("kick") - data class Kick( - val participantId: String, - val reason: String = "kicked") : JamMessage() - - @Serializable - @SerialName("leave") - data class Leave(val reason: String = "user_left") : JamMessage() -} +data class JamQueue( + val items: List, + val shuffleEnabled: Boolean = false) /** - * Playback commands. Queue navigation and play/pause are global — seek, - * volume, shuffle and loop are local to each participant. + * A playback anchor: track [trackKey] (at queue [index]) was at [positionMs] at server + * time [serverTimeMs]. While [advancing], everyone extrapolates the position from it at + * [rate]; a buffering host sends a non-advancing anchor so guests don't run ahead. */ @Serializable -sealed class PlaybackCmd { +data class JamAnchor( + val trackKey: String, + val index: Int, + val positionMs: Long, + val serverTimeMs: Long, + /** Room-wide play/pause intent; guests follow every change of it. */ + val playing: Boolean, + val advancing: Boolean = playing, + val rate: Float = 1f, + /** The audio source the host plays [trackKey] from, so guests stream the same file. */ + val source: PluginTaggedAudioSource? = null) { + /** The room's position at server time [serverNowMs]. */ + fun positionAt(serverNowMs: Long): Long = + if (advancing) positionMs + ((serverNowMs - serverTimeMs) * rate).toLong() else positionMs +} + +/** Playback control and suggestions. Volume, loop and the local queue view stay local. */ +@Serializable +sealed class JamCommand { @Serializable @SerialName("setPlaying") - data class SetPlaying(val playing: Boolean) : PlaybackCmd() + data class SetPlaying(val playing: Boolean) : JamCommand() @Serializable @SerialName("skipNext") - data object SkipNext : PlaybackCmd() + data object SkipNext : JamCommand() @Serializable @SerialName("skipPrevious") - data object SkipPrevious : PlaybackCmd() + data object SkipPrevious : JamCommand() @Serializable @SerialName("jumpTo") - data class JumpTo(val index: Int) : PlaybackCmd() -} + data class JumpTo(val index: Int) : JamCommand() -/** Retained per-participant presence entry (with an MQTT Last Will for leave). */ -@Serializable -data class JamPresence( - val clientId: String, - val displayName: String, - val isHost: Boolean, - val left: Boolean = false) + @Serializable + @SerialName("seekTo") + data class SeekTo(val positionMs: Long) : JamCommand() + + @Serializable + @SerialName("suggest") + data class Suggest( + val items: List, + val addedBy: String = "") : JamCommand() +} @Serializable data class JamMediaItem( @@ -117,6 +99,9 @@ data class JamMediaItem( val coverUrl: String, val protocol: String, val addedBy: String = "") { + /** Identifies the track across devices: the metadata id, or the file for local tracks. */ + val key: String get() = trackId.ifBlank { "local:$url" } + companion object { fun fromQueueEntry(entry: QueueEntry): JamMediaItem = when (entry) { is QueueEntry.StreamingTrack -> JamMediaItem( diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt index 07810202..c204ede2 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomClient.kt @@ -18,22 +18,31 @@ package org.spotube.spotube.core.jam import co.touchlab.kermit.Logger -import com.ditchoom.buffer.Charset -import com.ditchoom.buffer.codec.asReadBuffer -import com.ditchoom.buffer.toReadBuffer -import com.ditchoom.mqtt.client.ConnectionState -import com.ditchoom.mqtt.client.MqttClient -import com.ditchoom.mqtt.connection.MqttConnectionOptions -import com.ditchoom.mqtt.controlpacket.OpaquePublishPayloadCodec -import com.ditchoom.mqtt.controlpacket.QualityOfService -import com.ditchoom.mqtt.controlpacket.TopicName -import com.ditchoom.mqtt.controlpacket.WillConfig -import com.ditchoom.mqtt5.controlpacket.ConnectionRequest -import org.spotube.spotube.modules.settings.JamBroker +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocketSession +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.URLBuilder +import io.ktor.http.URLProtocol +import io.ktor.http.appendPathSegments +import io.ktor.http.isSuccess +import io.ktor.websocket.DefaultWebSocketSession +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readText +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -41,47 +50,74 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonClassDiscriminator import org.jetbrains.compose.resources.getString +import org.spotube.spotube.core.network.HttpClientFactory import spotube.composeapp.generated.resources.Res -import spotube.composeapp.generated.resources.jam_error_broker_host_empty -import spotube.composeapp.generated.resources.jam_error_broker_unreachable +import spotube.composeapp.generated.resources.jam_error_banned import spotube.composeapp.generated.resources.jam_error_connection_failed import spotube.composeapp.generated.resources.jam_error_connection_lost -import spotube.composeapp.generated.resources.jam_error_no_broker -import spotube.composeapp.generated.resources.settings_jam_broker_test_ok -import org.koin.core.component.KoinComponent -import kotlin.time.Duration.Companion.seconds +import spotube.composeapp.generated.resources.jam_error_kicked +import spotube.composeapp.generated.resources.jam_error_replaced +import spotube.composeapp.generated.resources.jam_error_room_full +import spotube.composeapp.generated.resources.jam_error_room_not_found +import spotube.composeapp.generated.resources.jam_error_server_incompatible +import spotube.composeapp.generated.resources.jam_error_server_unreachable +import spotube.composeapp.generated.resources.settings_jam_server_test_ok +import kotlin.time.Clock /** - * Thin facade over the Ditchoom MQTT 5 client for one jam room. + * WebSocket client for one jam room on the room server (`jam-server/`, a Cloudflare + * Durable Object per room code). * - * Topics (room code `C`): - * - `spotube/jam/{C}/state` retained — [JamMessage.QueueState] (host -> everyone) - * - `spotube/jam/{C}/cmd` volatile — commands/suggestions (everyone -> host, host -> guest) - * - `spotube/jam/{C}/presence/{clientId}` retained — [JamPresence], with a Last Will - * (`left = true`) so a dropped client disappears from the room automatically. + * The server keeps the host's latest queue and anchor for late joiners, routes guest + * commands to the host and is the clock every participant syncs playback against + * ([clock]). A dropped connection is retried with the same client id, which the + * server treats as the same participant (a host keeps the room for a grace period). * - * The library keeps the connection alive (auto-reconnect + backoff); this class only - * re-establishes the room subscription and re-publishes presence after a reconnect. + * The room is billed per incoming message, and on phones every transmission wakes the + * cellular radio for seconds, so the steady state sends almost nothing: one keep-alive + * a minute (answered by the server runtime for free) and a clock re-sync every + * [CLOCK_RESYNC_INTERVAL_MS]. Reconnect attempts back off to [RECONNECT_BACKOFF_MS]'s + * last step while the network stays away. */ -class JamRoomClient : KoinComponent { +class JamRoomClient(httpClientFactory: HttpClientFactory) { private val log = Logger.withTag("JamRoomClient") - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + // Nothing here may take the app down: a network failure is a reconnect, not a crash. + private val scope = CoroutineScope( + SupervisorJob() + Dispatchers.IO + + CoroutineExceptionHandler { _, e -> log.w(e) { "Unexpected jam connection failure" } }) private val json = Json { ignoreUnknownKeys = true classDiscriminator = "type" encodeDefaults = true } + private val http = httpClientFactory.create { + install(WebSockets) + install(HttpTimeout) { connectTimeoutMillis = CONNECT_TIMEOUT_MS } + } + private val plainHttp = httpClientFactory.create { + install(HttpTimeout) { requestTimeoutMillis = CONNECT_TIMEOUT_MS } + } - private var client: MqttClient? = null + val clock = JamClock() - private var roomCode: String? = null - private var localPresence: JamPresence? = null + private var session: DefaultWebSocketSession? = null + private var connectionJob: Job? = null + private var reader: Deferred? = null + private var pingId = 0 + /** When the current socket last received anything; a silent socket is a dead one. */ + private var lastFrameAtMs = 0L + /** The room's host as last reported by the server. */ + private var hostId: String? = null + private val pongs = MutableStateFlow(0) private val _isConnected = MutableStateFlow(false) val isConnected: StateFlow = _isConnected.asStateFlow() @@ -89,244 +125,413 @@ class JamRoomClient : KoinComponent { private val _connectionError = MutableStateFlow(null) val connectionError: StateFlow = _connectionError.asStateFlow() - private val _state = MutableSharedFlow(replay = 1, extraBufferCapacity = 8) - val state: SharedFlow = _state.asSharedFlow() - - private val _commands = MutableSharedFlow(extraBufferCapacity = 32) - val commands: SharedFlow = _commands.asSharedFlow() - - private val _presence = MutableStateFlow>(emptyMap()) - val presence: StateFlow> = _presence.asStateFlow() + private val _events = MutableSharedFlow(extraBufferCapacity = 64) + val events: SharedFlow = _events.asSharedFlow() // ---------- Public API ---------- - /** One-off connection check used by the settings screen. Returns latency description. */ - suspend fun testConnection(broker: JamBroker): Result { - if (broker.host.isBlank()) return Result.failure(IllegalArgumentException(getString(Res.string.jam_error_broker_host_empty))) + /** Checks that [serverUrl] runs a compatible room server. Returns a latency description. */ + suspend fun testConnection(serverUrl: String): Result = runCatching { val started = kotlin.time.TimeSource.Monotonic.markNow() - return runCatching { - val client = startClient(broker, clientId = "${broker.clientIdPrefix}-test") - try { - withTimeout(broker.connectionTimeoutSeconds.seconds) { - client.awaitConnectivity() - } - getString(Res.string.settings_jam_broker_test_ok, started.elapsedNow().inWholeMilliseconds) - } finally { - runCatching { client.shutdown(sendDisconnect = true, drain = false) } - } - } + val response = plainHttp.get(healthUrl(serverUrl)) + if (!response.status.isSuccess()) error(getString(Res.string.jam_error_server_unreachable)) + val health = json.decodeFromString(Health.serializer(), response.bodyAsText()) + if (!health.ok || health.v != PROTOCOL_VERSION) error(getString(Res.string.jam_error_server_incompatible)) + getString(Res.string.settings_jam_server_test_ok, started.elapsedNow().inWholeMilliseconds) } - /** Connects to [code] on [broker] and starts routing room messages. */ + /** + * Opens room [code] on [serverUrl] and syncs the clock. Fails with [JamRoomException] + * when the server refuses (code taken, room not found, banned, …). Once connected, + * drops are retried in the background until [disconnect] or a refusal. + */ suspend fun connect( - broker: JamBroker, + serverUrl: String, code: String, clientId: String, displayName: String, - isHost: Boolean): Result { + create: Boolean): Result { disconnect() - if (broker.host.isBlank()) { - return Result.failure(IllegalArgumentException(getString(Res.string.jam_error_no_broker))) - } - return runCatching { - roomCode = code - localPresence = JamPresence( - clientId = clientId, - displayName = displayName, - isHost = isHost, - left = false) - - val mqtt = startClient(broker, clientId) - client = mqtt - try { - withTimeout(broker.connectionTimeoutSeconds.seconds) { - // connectionState replays its current value, unlike awaitConnectivity's - // broadcast flow, which loses a CONNACK that lands before it is collected. - mqtt.connectionState.first { it is ConnectionState.Connected } - } - } catch (e: TimeoutCancellationException) { - throw IllegalStateException( - getString(Res.string.jam_error_broker_unreachable, broker.host, broker.port), e) + clock.reset() + val welcome = runCatching { open(serverUrl, code, clientId, displayName, create) } + .onFailure { e -> + log.w(e) { "Failed to open jam room $code" } + _connectionError.value = e.message ?: getString(Res.string.jam_error_connection_failed) } - _connectionError.value = null - log.i { "Connected to ${broker.host}:${broker.port} room=$code as $clientId" } + .getOrElse { return Result.failure(it) } - mqtt.connectionState - .onEach { onConnectionStateChanged(it) } - .launchIn(scope) - Unit - }.onFailure { e -> - log.w(e) { "Failed to connect to jam broker" } - _connectionError.value = e.message ?: getString(Res.string.jam_error_connection_failed) - _isConnected.value = false - runCatching { client?.shutdown(sendDisconnect = true, drain = false) } - client = null + connectionJob = scope.launch { + var current = welcome.second + var attempt = 0 + while (isActive) { + val refusal = runCatching { current.await() }.getOrNull() + ensureActive() + _isConnected.value = false + if (refusal != null) { + fail(refusal) + return@launch + } + _connectionError.value = getString(Res.string.jam_error_connection_lost) + // Rejoin: the room exists now, so even a former host comes back with "join". + while (isActive) { + delay(RECONNECT_BACKOFF_MS[attempt.coerceAtMost(RECONNECT_BACKOFF_MS.lastIndex)]) + attempt++ + var reopened = runCatching { open(serverUrl, code, clientId, displayName, create = false) } + // Everyone was gone long enough for the room to expire: its host brings it back. + if ((reopened.exceptionOrNull() as? JamRoomException)?.code == "room_not_found" && hostId == clientId) { + log.i { "Jam room $code expired while offline; recreating it" } + reopened = runCatching { open(serverUrl, code, clientId, displayName, create = true) } + } + val error = reopened.exceptionOrNull() + if (error is JamRoomException) { + fail(error.code) + return@launch + } + val (event, socket) = reopened.getOrNull() ?: continue + attempt = 0 + current = socket + _events.emit(event) + break + } + } } + return Result.success(welcome.first) } - suspend fun publishState(state: JamMessage.QueueState) { - val code = roomCode ?: return - publishJson(stateTopic(code), json.encodeToString(JamMessage.QueueState.serializer(), state), retain = true) - } + fun sendQueue(queue: JamQueue) = send(ClientFrame.Queue(queue)) - suspend fun publishCommand(message: JamMessage) { - val code = roomCode ?: return - publishJson(cmdTopic(code), json.encodeToString(JamMessage.serializer(), message), retain = false) - } + fun sendAnchor(anchor: JamAnchor) = send(ClientFrame.Anchor(anchor)) - /** Publishes our own (retained) presence. Re-published after every reconnect. */ - suspend fun publishPresence() { - val code = roomCode ?: return - val presence = localPresence ?: return - publishJson( - presenceTopic(code, presence.clientId), - json.encodeToString(JamPresence.serializer(), presence), - retain = true) - } + fun sendCommand(command: JamCommand) = send(ClientFrame.Cmd(command)) - /** Marks us as the host in presence (host takeover). */ - suspend fun claimHost() { - val presence = localPresence ?: return - localPresence = presence.copy(isHost = true) - publishPresence() - } - - /** Graceful leave: publish `left = true` before disconnecting. */ - suspend fun leavePresence() { - val presence = localPresence ?: return - val code = roomCode ?: return - runCatching { - publishJson( - presenceTopic(code, presence.clientId), - json.encodeToString(JamPresence.serializer(), presence.copy(left = true)), - retain = true) - } - } + fun kick(participantId: String, ban: Boolean) = send(ClientFrame.Kick(participantId, ban)) suspend fun disconnect() { - roomCode = null - localPresence = null + connectionJob?.cancel() + connectionJob = null + hostId = null + reader?.cancel() + reader = null + val current = session + session = null _isConnected.value = false - _presence.value = emptyMap() - val current = client - client = null - runCatching { current?.shutdown(sendDisconnect = true, drain = false) } + _connectionError.value = null + runCatching { current?.close() } } // ---------- Internals ---------- - private suspend fun startClient(broker: JamBroker, clientId: String): MqttClient { - val connection = MqttConnectionOptions.SocketConnection( - host = broker.host, - port = broker.port, - tlsEnabled = broker.useTls, - connectionTimeout = broker.connectionTimeoutSeconds.seconds) - val code = roomCode ?: "unset" - val will = WillConfig.Enabled( - topic = TopicName.fromOrThrow(presenceTopic(code, clientId)), - payload = json.encodeToString( - JamPresence.serializer(), - JamPresence(clientId, localPresence?.displayName ?: clientId, isHost = false, left = true)).toReadBuffer(Charset.UTF8), - qos = QualityOfService.AT_LEAST_ONCE, - retain = true) - val request = ConnectionRequest( - clientId = clientId, - keepAliveSeconds = broker.keepAliveSeconds, - cleanStart = true, - userName = broker.username, - password = broker.password, - will = will) - val persistence = request.controlPacketFactory.defaultPersistence(inMemory = true) - val brokerRef = persistence.addBroker(connection, request) - return MqttClient.start(scope = scope, broker = brokerRef, persistence = persistence) + /** + * Connects, waits for the welcome (or refusal) and syncs the clock. Returns the welcome + * and the socket's reader, which completes with the server's refusal (if any) on close. + */ + private suspend fun open( + serverUrl: String, + code: String, + clientId: String, + displayName: String, + create: Boolean): Pair> { + val socket = runCatching { http.webSocketSession(roomUrl(serverUrl, code, clientId, displayName, create)) } + .getOrElse { throw IllegalStateException(getString(Res.string.jam_error_server_unreachable), it) } + try { + val welcome = withTimeoutOrNull(CONNECT_TIMEOUT_MS) { awaitWelcome(socket) } + ?: throw IllegalStateException(getString(Res.string.jam_error_server_unreachable)) + if (welcome.v != PROTOCOL_VERSION) { + throw IllegalStateException(getString(Res.string.jam_error_server_incompatible)) + } + + session = socket + hostId = welcome.host + lastFrameAtMs = now() + _isConnected.value = true + _connectionError.value = null + // Read from here on: a pong's arrival time is part of the clock measurement. + val socketReader = scope.async { pump(socket) } + reader = socketReader + syncClock(socket) + log.i { "Joined jam room $code as $clientId (host=${welcome.host}, rtt=${clock.rttMs}ms)" } + return welcome.toEvent() to socketReader + } catch (e: Throwable) { + runCatching { socket.close() } + throw e + } } - private fun onConnectionStateChanged(state: ConnectionState) { - when (state) { - is ConnectionState.Connected -> { - _isConnected.value = true - _connectionError.value = null - scope.launch { - // Every connection (initial + reconnects) must re-establish the - // broker-side subscription (clean session) and re-publish our - // retained presence to clear any Last Will. Re-subscribing with - // the same filter replaces the previous dispatcher handler. - subscribeRoom() - publishPresence() - } - } - - ConnectionState.Disconnected, ConnectionState.Handshaking -> { - _isConnected.value = false - } - - else -> { - _isConnected.value = false - scope.launch { - val message = getString(Res.string.jam_error_connection_lost) - if (!_isConnected.value) _connectionError.value = message - } + private suspend fun awaitWelcome(socket: DefaultWebSocketSession): ServerFrame.Welcome { + while (true) { + val frame = socket.incoming.receiveCatching().getOrNull() + ?: throw IllegalStateException(getString(Res.string.jam_error_connection_failed)) + when (val decoded = decode(frame)) { + is ServerFrame.Welcome -> return decoded + is ServerFrame.Error -> throw JamRoomException(decoded.code, refusalMessage(decoded.code)) + else -> Unit } } } - private suspend fun subscribeRoom() { - val mqtt = client ?: return - val code = roomCode ?: return - val operation = mqtt.subscribe( - roomFilter(code), - OpaquePublishPayloadCodec, - QualityOfService.AT_LEAST_ONCE) { publish, payload -> - route(publish.topic.toString(), payload) + /** + * A burst of clock pings (playback can't sync before the clock is known); waits for + * their pongs, so the first sync is done when this returns. + */ + private suspend fun syncClock(socket: DefaultWebSocketSession, pings: Int = INITIAL_PINGS) { + val target = pongs.value + pings + repeat(pings) { + ping(socket) + delay(PING_SPACING_MS) } - runCatching { operation.subAck.await() } - .onFailure { log.w(it) { "Subscribe ack failed for room $code" } } + withTimeoutOrNull(CLOCK_SYNC_TIMEOUT_MS) { pongs.first { it >= target } } } - private fun route(topic: String, payload: com.ditchoom.mqtt.controlpacket.OpaquePublishPayload) { - val text = runCatching { - val buffer = payload.handle.asReadBuffer() - buffer.readString(buffer.remaining(), Charset.UTF8) - }.getOrElse { e -> - log.w(e) { "Failed to read jam payload on $topic" } - return + /** + * Reads frames until the socket closes or fails (a dropped network surfaces here as + * an exception). Returns the server's refusal, if any. + */ + private suspend fun pump(socket: DefaultWebSocketSession): String? = withKeepAlive(socket) { + var refusal: String? = null + try { + for (frame in socket.incoming) { + lastFrameAtMs = now() + refusal = handle(frame) ?: refusal + } + } catch (e: Exception) { + currentCoroutineContext().ensureActive() + log.i { "Jam connection dropped: ${e.message}" } } + refusal + } - runCatching { - when { - topic.endsWith("/state") -> { - _state.tryEmit(json.decodeFromString(JamMessage.QueueState.serializer(), text)) + /** + * Keeps the socket alive (and NAT mappings open) with free keep-alives, gives up on a + * socket that has gone silent (a half-open connection never errors by itself), and + * re-syncs the clock now and then. + */ + private suspend fun withKeepAlive( + socket: DefaultWebSocketSession, + block: suspend () -> T): T { + val keeper = scope.launch { + var lastClockSyncMs = now() + while (isActive) { + delay(KEEPALIVE_INTERVAL_MS) + if (now() - lastFrameAtMs > DEAD_AFTER_MS) { + log.i { "Jam connection went silent; reconnecting" } + runCatching { socket.close() } + socket.cancel() + return@launch } - - topic.endsWith("/cmd") -> { - _commands.tryEmit(json.decodeFromString(JamMessage.serializer(), text)) - } - - topic.contains("/presence/") -> { - val presence = json.decodeFromString(JamPresence.serializer(), text) - _presence.value = _presence.value + (presence.clientId to presence) + runCatching { socket.send(Frame.Text(KEEPALIVE_PING)) } + if (now() - lastClockSyncMs >= CLOCK_RESYNC_INTERVAL_MS) { + lastClockSyncMs = now() + runCatching { syncClock(socket, RESYNC_PINGS) } } } - }.onFailure { e -> - log.w(e) { "Failed to decode jam message on $topic: $text" } + } + return try { + block() + } finally { + keeper.cancel() } } - private suspend fun publishJson(topic: String, payload: String, retain: Boolean) { - val mqtt = client ?: return - runCatching { - mqtt.publish( - topicName = topic, - qos = QualityOfService.AT_LEAST_ONCE, - payload = payload.toReadBuffer(Charset.UTF8), - retain = retain) - }.onFailure { e -> - log.w(e) { "Failed to publish to $topic" } + /** Routes one frame; returns an error code when the server refused us. */ + private suspend fun handle(frame: Frame): String? { + if (frame is Frame.Text && frame.readText() == KEEPALIVE_PONG) return null + when (val decoded = decode(frame)) { + is ServerFrame.Pong -> { + clock.addSample(decoded.c, decoded.s, now()) + pongs.value++ + } + is ServerFrame.Welcome -> { + hostId = decoded.host + _events.emit(decoded.toEvent()) + } + is ServerFrame.Presence -> { + hostId = decoded.host + _events.emit(JamRoomEvent.Presence(decoded.host, decoded.participants.toParticipants(decoded.host))) + } + is ServerFrame.Queue -> _events.emit(JamRoomEvent.Queue(decoded.body)) + is ServerFrame.Anchor -> _events.emit(JamRoomEvent.Anchor(decoded.body)) + is ServerFrame.Cmd -> _events.emit(JamRoomEvent.Command(decoded.from, decoded.body)) + is ServerFrame.Error -> return decoded.code + null -> Unit + } + return null + } + + private suspend fun fail(code: String) { + val message = refusalMessage(code) + log.i { "Jam room closed by the server: $code" } + session = null + _connectionError.value = message + _events.emit(JamRoomEvent.Closed(code, message)) + } + + private suspend fun ping(socket: DefaultWebSocketSession) { + socket.send(Frame.Text(json.encodeToString(ClientFrame.serializer(), ClientFrame.Ping(++pingId, now())))) + } + + private fun send(frame: ClientFrame) { + val socket = session ?: return + val text = json.encodeToString(ClientFrame.serializer(), frame) + scope.launch { + runCatching { socket.send(Frame.Text(text)) } + .onFailure { log.w(it) { "Failed to send jam frame" } } } } - private fun stateTopic(code: String) = "spotube/jam/$code/state" - private fun cmdTopic(code: String) = "spotube/jam/$code/cmd" - private fun presenceTopic(code: String, clientId: String) = "spotube/jam/$code/presence/$clientId" - private fun roomFilter(code: String) = "spotube/jam/$code/#" + private fun decode(frame: Frame): ServerFrame? { + if (frame !is Frame.Text) return null + val text = frame.readText() + return runCatching { json.decodeFromString(ServerFrame.serializer(), text) } + .onFailure { log.w(it) { "Failed to decode jam frame: ${text.take(200)}" } } + .getOrNull() + } + + private suspend fun refusalMessage(code: String): String = when (code) { + "room_not_found" -> getString(Res.string.jam_error_room_not_found) + "room_full" -> getString(Res.string.jam_error_room_full) + "banned" -> getString(Res.string.jam_error_banned) + "kicked" -> getString(Res.string.jam_error_kicked) + "replaced" -> getString(Res.string.jam_error_replaced) + else -> getString(Res.string.jam_error_connection_failed) + } + + private fun roomUrl(serverUrl: String, code: String, clientId: String, displayName: String, create: Boolean) = + URLBuilder(serverUrl.trim()).apply { + protocol = if (protocol == URLProtocol.HTTP || protocol == URLProtocol.WS) URLProtocol.WS else URLProtocol.WSS + appendPathSegments("v1", "rooms", code) + parameters.append("id", clientId) + parameters.append("name", displayName) + parameters.append("mode", if (create) "create" else "join") + }.buildString() + + private fun healthUrl(serverUrl: String) = + URLBuilder(serverUrl.trim()).apply { + if (protocol == URLProtocol.WS) protocol = URLProtocol.HTTP + if (protocol == URLProtocol.WSS) protocol = URLProtocol.HTTPS + appendPathSegments("v1", "health") + }.buildString() + + private fun now(): Long = Clock.System.now().toEpochMilliseconds() + + companion object { + const val PROTOCOL_VERSION = 1 + private const val CONNECT_TIMEOUT_MS = 10_000L + private const val INITIAL_PINGS = 6 + private const val RESYNC_PINGS = 3 + private const val PING_SPACING_MS = 100L + private const val CLOCK_SYNC_TIMEOUT_MS = 3_000L + /** Clocks drift by a few ms per minute at most; this keeps sync error well under 50 ms. */ + private const val CLOCK_RESYNC_INTERVAL_MS = 10 * 60_000L + /** + * As rare as possible (each one wakes a phone's radio) while staying under the + * 100 s idle timeout of Cloudflare's proxy. A dropped idle socket would only cost + * a reconnect, so there's no need for margin beyond that. + */ + private const val KEEPALIVE_INTERVAL_MS = 60_000L + /** No pong to two keep-alives in a row: the connection is gone. */ + private const val DEAD_AFTER_MS = 2 * KEEPALIVE_INTERVAL_MS + 10_000L + private const val KEEPALIVE_PING = "ping" + private const val KEEPALIVE_PONG = "pong" + /** Quick at first (most drops are a network switch), then slower to spare the battery. */ + private val RECONNECT_BACKOFF_MS = longArrayOf(500, 1_000, 2_000, 4_000, 8_000, 8_000, 15_000, 15_000, 30_000, 60_000) + } +} + +/** Something that happened in the room, for [JamRoomService]. */ +sealed interface JamRoomEvent { + /** (Re)joined the room; carries the retained queue and anchor. */ + data class Welcome( + val you: String, + val host: String?, + val participants: List, + val queue: JamQueue?, + val anchor: JamAnchor?) : JamRoomEvent + + data class Presence(val host: String?, val participants: List) : JamRoomEvent + data class Queue(val queue: JamQueue) : JamRoomEvent + data class Anchor(val anchor: JamAnchor) : JamRoomEvent + data class Command(val from: String, val command: JamCommand) : JamRoomEvent + + /** The server ended our membership ([code]: kicked, banned, …); no retry follows. */ + data class Closed(val code: String, val message: String) : JamRoomEvent +} + +/** The server refused to let us into the room. */ +class JamRoomException(val code: String, message: String) : Exception(message) + +// ---------- Wire frames (see jam-server/src/protocol.ts) ---------- + +@Serializable +private data class Health(val ok: Boolean = false, val v: Int = 0) + +@Serializable +private data class WireParticipant(val id: String, val name: String) + +private fun List.toParticipants(host: String?) = + map { JamParticipant(it.id, it.name, isHost = it.id == host) } + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +@JsonClassDiscriminator("t") +private sealed class ClientFrame { + @Serializable + @SerialName("ping") + data class Ping(val id: Int, val c: Long) : ClientFrame() + + @Serializable + @SerialName("queue") + data class Queue(val body: JamQueue) : ClientFrame() + + @Serializable + @SerialName("anchor") + data class Anchor(val body: JamAnchor) : ClientFrame() + + @Serializable + @SerialName("cmd") + data class Cmd(val body: JamCommand) : ClientFrame() + + @Serializable + @SerialName("kick") + data class Kick(val target: String, val ban: Boolean) : ClientFrame() +} + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +@JsonClassDiscriminator("t") +private sealed class ServerFrame { + @Serializable + @SerialName("welcome") + data class Welcome( + val v: Int, + val you: String, + val host: String? = null, + val participants: List = emptyList(), + val queue: JamQueue? = null, + val anchor: JamAnchor? = null) : ServerFrame() { + fun toEvent() = JamRoomEvent.Welcome(you, host, participants.toParticipants(host), queue, anchor) + } + + @Serializable + @SerialName("pong") + data class Pong(val id: Int, val c: Long, val s: Long) : ServerFrame() + + @Serializable + @SerialName("presence") + data class Presence(val host: String? = null, val participants: List = emptyList()) : ServerFrame() + + @Serializable + @SerialName("queue") + data class Queue(val body: JamQueue) : ServerFrame() + + @Serializable + @SerialName("anchor") + data class Anchor(val body: JamAnchor) : ServerFrame() + + @Serializable + @SerialName("cmd") + data class Cmd(val from: String, val body: JamCommand) : ServerFrame() + + @Serializable + @SerialName("error") + data class Error(val code: String) : ServerFrame() } diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomCode.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomCode.kt index f8d0f19b..e6ec412b 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomCode.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomCode.kt @@ -21,7 +21,7 @@ import kotlin.random.Random /** * Six-character room codes shared verbally / by text. Codes are opaque keys used - * to namespace the MQTT topics of a jam room — they carry no connection details. + * to name a jam room on the room server — they carry no connection details. * * The alphabet excludes look-alike characters (I, O, 0, 1) so codes are easy to * read aloud and retype. diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomService.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomService.kt index f317e6ec..abeb2ff4 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomService.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/JamRoomService.kt @@ -26,53 +26,74 @@ import org.spotube.spotube.core.audioplayer.AudioPlayerInterface import org.spotube.spotube.core.audioplayer.AudioPlayerQueue import org.spotube.spotube.core.audioplayer.PlayerState import org.spotube.spotube.core.audioplayer.QueueEntry -import org.spotube.spotube.modules.settings.SettingsRepository +import org.spotube.spotube.core.server.PluginTaggedAudioSource +import org.spotube.spotube.core.server.StreamingUrlRepository +import org.spotube.spotube.core.server.TrackSourceRepository +import org.spotube.spotube.modules.plugin.AudioPluginSource +import org.spotube.spotube.modules.settings.UserSettingsStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.math.abs import kotlin.random.Random import org.jetbrains.compose.resources.getString import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.jam_error_invalid_code -import spotube.composeapp.generated.resources.jam_error_no_broker +import spotube.composeapp.generated.resources.jam_error_no_server import kotlin.time.Clock +import kotlin.time.Duration.Companion.milliseconds /** - * A jam session over MQTT (star topology, host-authoritative queue). + * A jam session: one host, any number of guests, all playing the same thing at the same + * moment. * - * Sync rules: - * - The queue list and current index are global. [skipNext]/[skipPrevious]/[jumpTo] - * from anyone are applied by the host, then broadcast via the retained state topic. - * - Play/pause is global: anyone's play/pause is sent to the host, which applies it - * and broadcasts it; guests follow every change of the host's play state. - * - Seek, volume and loop are local to each device — never broadcast. - * - Shuffle is host-only; guests mirror the host's shuffle setting. - * - Guests can only add to the queue (suggest); the host applies suggestions. - * - If the host leaves, the participant with the lowest client id takes over. + * Sync model: + * - The host owns the queue and broadcasts it ([JamQueue]), plus a playback [JamAnchor] + * ("track X was at position P at server time T") whenever playback changes. Times are + * in the room server's clock, which every device estimates with [JamClock], so guests + * extrapolate an anchor on their own; the host only re-anchors when its player drifts + * from its last anchor (and rarely, as a safety net). Rooms are billed per message, + * so nothing is sent periodically. + * - Battery: nothing polls while there's nothing to follow. A paused room, a buffering + * host or a missing queue puts the guest's sync loop to sleep until the room or the + * player changes, and the host only checks its own drift while playing. + * - Guests follow the anchor's track and play state, and keep their position within + * ~150 ms of the room by nudging their playback rate a few percent ([JamDriftCorrector]), + * seeking only when far off. + * - Anyone's play/pause, skip, jump and seek go to the host, which applies them for the + * whole room. Volume and loop stay local. + * - Guests stream each track from the host's audio source when they use the same audio + * plugin, so everyone hears the same recording (different uploads don't line up). + * - The server hands hosting to the longest-connected guest when the host leaves. */ -private data class PlaybackBroadcast( - val queue: List, - val current: QueueEntry?, - val shuffle: Boolean, - val isPlaying: Boolean) - class JamRoomService( private val jamClient: JamRoomClient, private val audioPlayer: AudioPlayerInterface, private val audioPlayerQueue: AudioPlayerQueue, - private val settingsRepository: SettingsRepository) { + private val settingsRepository: UserSettingsStore, + private val audioPluginSource: AudioPluginSource, + private val trackSources: TrackSourceRepository, + private val streamingUrlRepository: StreamingUrlRepository) { private val log = Logger.withTag("JamRoomService") private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -119,27 +140,45 @@ class JamRoomService( val participantClientId: String get() = localClientId - private var hostBroadcastJob: Job? = null + private var roleJob: Job? = null + private var leaving = false + /** Joining or in a room: room events apply (some arrive before join returns). */ + private var inRoom = false - /** Guest side: last queue snapshot applied to the local player. */ - private var lastAppliedItems: List = emptyList() - private var lastAppliedIndex = -1 - - /** - * Host side: a song completed, so index changes within this window are - * auto-advance (guests must not follow). The completion event, the player - * state change and the media transition arrive as separate flow emissions, - * so the window covers the whole sequence instead of a single flag. - */ - private var autoAdvanceDeadlineMs = 0L - - /** Guest side: the room's play state from the last host broadcast (null until the first one). */ + /** Room-wide play state from the last anchor (null until the first one). */ private var lastHostIsPlaying: Boolean? = null - /** Host side: client ids banned for this session. */ - private val bannedClientIds = mutableSetOf() + /** Our playback speed before we joined, restored when sync stops changing it. */ + private var speedBeforeJam: Float? = null - private var leaving = false + // Latest room state from the host (kept in any role, applied while a guest) + private var roomQueue: JamQueue? = null + private var anchor: JamAnchor? = null + private var lastAppliedItems: List = emptyList() + private val corrector = JamDriftCorrector() + private val syncMutex = Mutex() + private var appliedRate: Float? = null + /** How far ahead to aim a seek so playback resumes on time; learned from each seek. */ + private var seekLeadMs = INITIAL_SEEK_LEAD_MS + private var lastCorrectionWasSeek = false + /** Don't correct before this (a seek or track change is still settling). */ + private var holdSyncUntilMs = 0L + private val pinnedSources = mutableSetOf() + /** Bumped on every anchor and queue from the host; wakes an idle guest sync loop. */ + private val roomVersion = MutableStateFlow(0) + + // Position tracking, used to extrapolate the player's coarse position reports + // and to tell user seeks apart from playback. + private var positionSampleMs = 0L + private var positionSampledAtMs = 0L + private var positionAdvancing = false + private var ownSeekUntilMs = 0L + private var entryChangedAtMs = 0L + + // The host's last anchor, to notice its own player drifting away from it. + private var sentAnchor: JamAnchor? = null + private var sentAnchorAtMs = 0L + private val hostDrifts = ArrayDeque() init { jamClient.isConnected @@ -148,19 +187,23 @@ class JamRoomService( jamClient.connectionError .onEach { _connectionError.value = it } .launchIn(scope) - jamClient.state - .onEach { onRemoteState(it) } + jamClient.events + .onEach { onEvent(it) } .launchIn(scope) - jamClient.commands - .onEach { onCommand(it) } + audioPlayerQueue.currentQueueEntryFlow + .onEach { entryChangedAtMs = now() } .launchIn(scope) - jamClient.presence - .onEach { onPresence(it) } + audioPlayer.positionFlow + .onEach { onPosition(it.inWholeMilliseconds) } .launchIn(scope) - // A naturally-completed song means the host's next index change is an - // auto-advance — guests must NOT follow those, only manual skips. - audioPlayer.completionFlow - .onEach { autoAdvanceDeadlineMs = now() + AUTO_ADVANCE_WINDOW_MS } + // Extrapolation restarts from wherever the player was when it started or stopped + // moving. (Not from positionFlow's value: a seek's report must stay a jump for onPosition.) + audioPlayer.playerStateFlow + .onEach { state -> + positionSampleMs = localPositionMs() + positionSampledAtMs = now() + positionAdvancing = state == PlayerState.PLAYING + } .launchIn(scope) // A guest's own play/pause (UI, notification, headset) goes to the host, which // applies it for the whole room. Intent that matches the room's state came from @@ -169,7 +212,7 @@ class JamRoomService( .onEach { playing -> val roomPlaying = lastHostIsPlaying if (_role.value == JamRole.Guest && !leaving && roomPlaying != null && playing != roomPlaying) { - publishCommand(PlaybackCmd.SetPlaying(playing)) + jamClient.sendCommand(JamCommand.SetPlaying(playing)) } } .launchIn(scope) @@ -178,95 +221,97 @@ class JamRoomService( // ---------- Session lifecycle ---------- suspend fun createRoom(): Result { - val broker = settingsRepository.userSettings.value.jamBroker - if (broker.host.isBlank()) { - return Result.failure(IllegalStateException(getString(Res.string.jam_error_no_broker))) - } - val code = JamRoomCode.generate() - localClientId = newClientId(broker) - val name = participantName("Host") - localDisplayName = name + val serverUrl = serverUrl() ?: return Result.failure(IllegalStateException(getString(Res.string.jam_error_no_server))) + localClientId = newClientId() + localDisplayName = participantName("Host") + enter() - return jamClient.connect( - broker = broker, - code = code, - clientId = localClientId, - displayName = name, - isHost = true).map { - _role.value = JamRole.Host - _roomCode.value = code - _participants.value = listOf(JamParticipant(localClientId, name, isHost = true)) - lastAppliedItems = emptyList() - lastAppliedIndex = -1 - bannedClientIds.clear() - leaving = false - autoAdvanceDeadlineMs = 0L - lastHostIsPlaying = null - startHostBroadcast() - persistLastCode(code) - code + // A fresh code is almost never taken; retry the rare collision with another one. + var result: Result + var code: String + var attempts = 0 + do { + code = JamRoomCode.generate() + result = jamClient.connect(serverUrl, code, localClientId, localDisplayName, create = true) + val taken = (result.exceptionOrNull() as? JamRoomException)?.code == "room_taken" + } while (taken && ++attempts < CREATE_ATTEMPTS) + + val welcome = result.getOrElse { + inRoom = false + return Result.failure(it) } + start(code, welcome) + return Result.success(code) } suspend fun joinRoom(code: String): Result { - val broker = settingsRepository.userSettings.value.jamBroker - if (broker.host.isBlank()) { - return Result.failure(IllegalStateException(getString(Res.string.jam_error_no_broker))) - } + val serverUrl = serverUrl() ?: return Result.failure(IllegalStateException(getString(Res.string.jam_error_no_server))) val normalized = JamRoomCode.normalize(code) if (!JamRoomCode.isValid(normalized)) { return Result.failure(IllegalArgumentException(getString(Res.string.jam_error_invalid_code))) } - localClientId = newClientId(broker) - val name = participantName("Guest") - localDisplayName = name + localClientId = newClientId() + localDisplayName = participantName("Guest") + enter() - return jamClient.connect( - broker = broker, - code = normalized, - clientId = localClientId, - displayName = name, - isHost = false).map { - _role.value = JamRole.Guest - _roomCode.value = normalized - _participants.value = emptyList() - lastAppliedItems = emptyList() - lastAppliedIndex = -1 - leaving = false - lastHostIsPlaying = null - persistLastCode(normalized) - } + val welcome = jamClient.connect(serverUrl, normalized, localClientId, localDisplayName, create = false) + .getOrElse { + inRoom = false + return Result.failure(it) + } + start(normalized, welcome) + return Result.success(Unit) } suspend fun leaveRoom() { leaving = true - stopHostBroadcast() - runCatching { jamClient.leavePresence() } jamClient.disconnect() + reset() + } + + private fun enter() { + inRoom = true + roomQueue = null + anchor = null + } + + private suspend fun start(code: String, welcome: JamRoomEvent.Welcome) { + leaving = false + lastHostIsPlaying = null + speedBeforeJam = audioPlayer.playbackSpeedFlow.value + _roomCode.value = code + _connectionError.value = null + persistLastCode(code) + // Frames that arrived while the clock synced are newer than the welcome's snapshot. + onWelcome(welcome.copy(queue = roomQueue ?: welcome.queue, anchor = anchor ?: welcome.anchor)) + } + + private suspend fun reset() { + inRoom = false + roleJob?.cancel() + roleJob = null + restoreSpeed() _role.value = null _roomCode.value = null _participants.value = emptyList() _shuffleEnabled.value = false _isConnected.value = false + roomQueue = null + anchor = null lastAppliedItems = emptyList() - lastAppliedIndex = -1 - bannedClientIds.clear() lastHostIsPlaying = null + pinnedSources.clear() + corrector.reset() + speedBeforeJam = null } // ---------- Controls (called from the UI) ---------- - fun skipNext() { - publishCommand(PlaybackCmd.SkipNext) - } + fun skipNext() = control(JamCommand.SkipNext) - fun skipPrevious() { - publishCommand(PlaybackCmd.SkipPrevious) - } + fun skipPrevious() = control(JamCommand.SkipPrevious) - fun jumpTo(index: Int) { - publishCommand(PlaybackCmd.JumpTo(index)) - } + fun jumpTo(index: Int) = control(JamCommand.JumpTo(index)) /** Host-only. Applied locally; the queue broadcast carries the new shuffle flag. */ fun toggleShuffle() { @@ -277,241 +322,454 @@ class JamRoomService( } suspend fun suggestTrack(track: MetadataTrack) { - jamClient.publishCommand( - JamMessage.SuggestTrack( - mediaItem = JamMediaItem.fromTrack(track), - addedBy = localClientId) - ) + jamClient.sendCommand(JamCommand.Suggest(listOf(JamMediaItem.fromTrack(track)), addedBy = localClientId)) } suspend fun suggestPlaylist(tracks: List) { if (tracks.isEmpty()) return - jamClient.publishCommand( - JamMessage.SuggestPlaylist( - tracks = tracks.map(JamMediaItem::fromTrack), - addedBy = localClientId) - ) + jamClient.sendCommand(JamCommand.Suggest(tracks.map(JamMediaItem::fromTrack), addedBy = localClientId)) } - suspend fun kickParticipant(participantId: String, reason: String = "kicked by host") { + suspend fun kickParticipant(participantId: String) { if (_role.value != JamRole.Host) return - jamClient.publishCommand(JamMessage.Kick(participantId, reason)) + jamClient.kick(participantId, ban = false) } suspend fun banParticipant(participantId: String) { if (_role.value != JamRole.Host) return - bannedClientIds += participantId - kickParticipant(participantId, "banned by host") + jamClient.kick(participantId, ban = true) } - // ---------- Host: broadcast ---------- + /** The host applies its own controls; a guest's go to the host. */ + private fun control(command: JamCommand) { + scope.launch { + if (_role.value == JamRole.Host) applyCommand(command) else jamClient.sendCommand(command) + } + } - private fun startHostBroadcast() { - if (hostBroadcastJob?.isActive == true) return - hostBroadcastJob = scope.launch { - combine( - audioPlayerQueue.queueFlow, - audioPlayerQueue.currentQueueEntryFlow, - audioPlayer.shuffleModeFlow, - playIntent) { queue, current, shuffle, isPlaying -> - PlaybackBroadcast(queue, current, shuffle, isPlaying) + // ---------- Room events ---------- + + private suspend fun onEvent(event: JamRoomEvent) { + if (!inRoom) return + when (event) { + is JamRoomEvent.Welcome -> onWelcome(event) + is JamRoomEvent.Presence -> { + onMembership(event.host, event.participants) + // The server keeps anchors in memory only; a joiner of a room that was + // asleep gets none in its welcome, so the host answers every join. + if (_role.value == JamRole.Host) publishAnchor() } - .onEach { broadcast -> - if (_role.value != JamRole.Host) return@onEach - val index = if (broadcast.current != null) { - broadcast.queue.indexOfFirst { it.matchesEntry(broadcast.current) } - } else { - -1 + is JamRoomEvent.Queue -> { + roomQueue = event.queue + if (_role.value == JamRole.Guest) applyQueue(event.queue) + } + is JamRoomEvent.Anchor -> { + anchor = event.anchor + if (_role.value == JamRole.Guest) onAnchor(event.anchor) + } + is JamRoomEvent.Command -> if (_role.value == JamRole.Host) applyCommand(event.command) + is JamRoomEvent.Closed -> { + log.i { "Left jam room: ${event.code}" } + reset() + _connectionError.value = event.message + } + } + } + + /** First join and every reconnect: the server's view of the room replaces ours. */ + private fun onWelcome(welcome: JamRoomEvent.Welcome) { + welcome.queue?.let { roomQueue = it } + welcome.anchor?.let { anchor = it } + onMembership(welcome.host, welcome.participants, force = true) + } + + private fun onMembership(host: String?, participants: List, force: Boolean = false) { + _participants.value = participants + val role = if (host == localClientId) JamRole.Host else JamRole.Guest + // A (re)welcome restarts the role so a guest re-applies the room's state. + if (role == _role.value && !force) return + log.i { "Jam role: ${_role.value} -> $role" } + _role.value = role + roleJob?.cancel() + roleJob = scope.launch { + if (role == JamRole.Host) runHost() else runGuest() + } + } + + // ---------- Host ---------- + + private suspend fun runHost() { + restoreSpeed() + sentAnchor = null + kotlinx.coroutines.coroutineScope { + combine(audioPlayerQueue.queueFlow, audioPlayer.shuffleModeFlow) { queue, shuffle -> + JamQueue(queue.map(JamMediaItem::fromQueueEntry), shuffle) + } + .distinctUntilChanged() + .onEach { queue -> + _shuffleEnabled.value = queue.shuffleEnabled + // After a reconnect the server already has it: skip the re-upload. + if (queue != roomQueue) { + roomQueue = queue + jamClient.sendQueue(queue) } - _shuffleEnabled.value = broadcast.shuffle - jamClient.publishState( - JamMessage.QueueState( - items = broadcast.queue.map(JamMediaItem::fromQueueEntry), - currentIndex = index.coerceAtLeast(0), - shuffleEnabled = broadcast.shuffle, - follow = now() > autoAdvanceDeadlineMs, - isPlaying = broadcast.isPlaying) - ) } .launchIn(this) + + combine( + audioPlayerQueue.currentQueueEntryFlow, + playIntent, + audioPlayer.playerStateFlow, + audioPlayer.playbackSpeedFlow) { _, _, _, _ -> } + .onEach { publishAnchor() } + .launchIn(this) + + // Drift only builds up while playing; a paused host checks nothing. + launch { + while (isActive) { + audioPlayer.playerStateFlow.first { it == PlayerState.PLAYING } + delay(HOST_DRIFT_CHECK_INTERVAL_MS) + checkHostDrift() + } + } } } - private fun stopHostBroadcast() { - hostBroadcastJob?.cancel() - hostBroadcastJob = null - } - - // ---------- Guest: apply remote state ---------- - - private suspend fun onRemoteState(state: JamMessage.QueueState) { - if (_role.value != JamRole.Guest) return - if (leaving) return - - _shuffleEnabled.value = state.shuffleEnabled - runCatching { audioPlayer.shuffle(state.shuffleEnabled) } - - // Play/pause is room-wide: follow every change of the host's play state. The room - // state is recorded first, so the resulting local intent isn't sent back as a command. - if (state.isPlaying != lastHostIsPlaying) { - lastHostIsPlaying = state.isPlaying - if (state.isPlaying != playIntent.value) { - runCatching { if (state.isPlaying) audioPlayer.play() else audioPlayer.pause() } - .onFailure { log.w(it) { "Failed to follow the host's play state" } } - } - } - - val items = state.items.filter { it.trackId.isNotBlank() || it.url.isNotBlank() } - - if (items != lastAppliedItems) { - val previous = lastAppliedItems - lastAppliedItems = items - val guestCurrent = audioPlayerQueue.currentQueueEntryFlow.value - val guestIndex = items.indexOfFirst { it.matchesEntry(guestCurrent) } - - // The host only appended items (e.g. accepted suggestions): merge - // them in without resetting playback or the guest's position. - if (previous.isNotEmpty() && items.size > previous.size && - items.take(previous.size) == previous && guestIndex >= 0 - ) { - lastAppliedIndex = guestIndex - val appended = items.drop(previous.size) - runCatching { - audioPlayerQueue.addAllToQueue(appended.map { it.toQueueEntry() }) - }.onFailure { log.w(it) { "Failed to append jam queue items" } } - return - } - - // Full re-sync. Keep the guest's current track when it still exists - // in the synced queue; otherwise take the host's position. - val startIndex = if (guestIndex >= 0) guestIndex - else state.currentIndex.coerceIn(0, items.lastIndex.coerceAtLeast(0)) - lastAppliedIndex = startIndex - runCatching { - audioPlayerQueue.load( - entries = items.map { it.toQueueEntry() }, - autoPlay = state.isPlaying, - startPosition = startIndex) - }.onFailure { log.w(it) { "Failed to apply jam queue" } } + /** + * Guests extrapolate the last anchor with the shared clock, so it stays valid as long + * as the host's player keeps pace with its own clock. When it doesn't (audio clock + * drift, a stall the player didn't report as buffering), re-anchor. Readings are + * smoothed like a guest's, since players report position coarsely. + */ + private suspend fun checkHostDrift() { + val last = sentAnchor ?: return + val now = now() + if (now - sentAnchorAtMs >= HOST_ANCHOR_HEARTBEAT_MS) { + publishAnchor() return } - - // Same queue content: only follow the host when it moved manually - // (skip/jump). Natural auto-advance keeps everyone where they are. - if (state.follow && state.currentIndex != lastAppliedIndex) { - lastAppliedIndex = state.currentIndex - runCatching { - audioPlayerQueue.jumpTo(state.currentIndex.coerceAtLeast(0), autoPlay = false) - }.onFailure { log.w(it) { "Failed to follow jam queue index" } } + if (!last.advancing || audioPlayer.playerStateFlow.value != PlayerState.PLAYING) { + hostDrifts.clear() + return + } + hostDrifts.addLast(localPositionMs() - last.positionAt(jamClient.clock.serverTime(now))) + while (hostDrifts.size > HOST_DRIFT_WINDOW) hostDrifts.removeFirst() + val smoothed = hostDrifts.sorted()[hostDrifts.size / 2] + if (hostDrifts.size == HOST_DRIFT_WINDOW && + abs(smoothed) > HOST_REANCHOR_DRIFT_MS && + now - sentAnchorAtMs >= HOST_REANCHOR_MIN_INTERVAL_MS + ) { + log.d { "Jam host drifted ${smoothed}ms from its anchor; re-anchoring" } + publishAnchor() } } - // ---------- Incoming commands ---------- - - private suspend fun onCommand(message: JamMessage) { - when (message) { - is JamMessage.PlaybackCommand -> { - if (_role.value != JamRole.Host) return - applyCommand(message.command) - } - - is JamMessage.SuggestTrack -> { - if (_role.value == JamRole.Host) { - acceptSuggestion(listOf(message.mediaItem.copy(addedBy = message.addedBy))) - } - } - - is JamMessage.SuggestPlaylist -> { - if (_role.value == JamRole.Host) { - acceptSuggestion(message.tracks.map { it.copy(addedBy = message.addedBy) }) - } - } - - is JamMessage.Kick -> { - if (_role.value == JamRole.Guest && message.participantId == localClientId) { - log.i { "Kicked from jam room: ${message.reason}" } - leaveRoom() - } - } - - else -> Unit - } + private suspend fun publishAnchor() { + if (_role.value != JamRole.Host) return + val current = audioPlayerQueue.currentQueueEntryFlow.value ?: return + val playing = playIntent.value + val next = JamAnchor( + trackKey = current.jamKey(), + index = audioPlayerQueue.queueFlow.value.indexOfFirst { it.jamKey() == current.jamKey() }, + positionMs = localPositionMs(), + serverTimeMs = jamClient.clock.serverTime(now()), + playing = playing, + advancing = playing && audioPlayer.playerStateFlow.value == PlayerState.PLAYING, + rate = audioPlayer.playbackSpeedFlow.value, + source = currentSource(current)) + sentAnchor = next + sentAnchorAtMs = now() + hostDrifts.clear() + jamClient.sendAnchor(next) } - private suspend fun applyCommand(command: PlaybackCmd) { - when (command) { - // Applied locally; the host broadcast carries the new play state to every guest - is PlaybackCmd.SetPlaying -> runCatching { - if (command.playing) audioPlayer.play() else audioPlayer.pause() + /** The source the host's audio plugin matched to [entry], so guests can use the same one. */ + private suspend fun currentSource(entry: QueueEntry): PluginTaggedAudioSource? { + if (entry !is QueueEntry.StreamingTrack) return null + val plugin = audioPluginSource.selectedAudioPlugin.value ?: return null + return runCatching { trackSources.getTrackSource(entry.track, plugin.pluginId) }.getOrNull() + ?.let { PluginTaggedAudioSource(plugin.pluginId, it) } + } + + private suspend fun applyCommand(command: JamCommand) { + runCatching { + when (command) { + // Applied locally; the anchor carries the change to every guest. + is JamCommand.SetPlaying -> if (command.playing) audioPlayer.play() else audioPlayer.pause() + JamCommand.SkipNext -> audioPlayer.skipToNext() + JamCommand.SkipPrevious -> audioPlayer.skipToPrevious() + is JamCommand.JumpTo -> audioPlayer.jumpTo(command.index) + is JamCommand.SeekTo -> { + seek(command.positionMs) + publishAnchor() + } + is JamCommand.Suggest -> acceptSuggestion(command.items.map { it.copy(addedBy = command.addedBy) }) } - PlaybackCmd.SkipNext -> runCatching { audioPlayer.skipToNext() } - PlaybackCmd.SkipPrevious -> runCatching { audioPlayer.skipToPrevious() } - is PlaybackCmd.JumpTo -> runCatching { audioPlayer.jumpTo(command.index) } - } + }.onFailure { log.w(it) { "Failed to apply jam command $command" } } } private suspend fun acceptSuggestion(items: List) { if (items.isEmpty()) return log.i { "Accepting ${items.size} suggested item(s) into the jam queue" } - runCatching { - audioPlayerQueue.addAllToQueue(items.map { it.toQueueEntry() }) + audioPlayerQueue.addAllToQueue(items.map { it.toQueueEntry() }) + } + + // ---------- Guest ---------- + + private suspend fun runGuest() { + // Guests follow the host's order through anchors; a local shuffle would only + // make auto-advance pick a different track for a moment. + runCatching { audioPlayer.shuffle(false) } + corrector.reset() + roomQueue?.let { applyQueue(it) } + anchor?.let { onAnchor(it) } + while (true) { + // Captured before the check, so a change during it still wakes the wait below. + val seenRoom = roomVersion.value + val seenState = audioPlayer.playerStateFlow.value + val seenQueue = audioPlayerQueue.queueFlow.value + val status = syncMutex.withLock { + runCatching { syncOnce() } + .onFailure { log.w(it) { "Jam sync failed" } } + .getOrDefault(SyncStatus.Busy) + } + when (status) { + // While playing the CPU is awake for audio anyway, so a fast check is free + // and catches drift early (slower checks made seeks settle visibly later). + SyncStatus.Busy, SyncStatus.InSync -> delay(SYNC_INTERVAL_MS) + // Sleep until the room or our player changes (the timeout is a safety net). + SyncStatus.Idle -> withTimeoutOrNull(IDLE_RECHECK_MS) { + merge( + roomVersion.filter { it != seenRoom }, + audioPlayer.playerStateFlow.filter { it != seenState }, + audioPlayerQueue.queueFlow.filter { it !== seenQueue }).first() + } + } } } - // ---------- Presence & host takeover ---------- + private enum class SyncStatus { + /** Correcting or settling: check again soon. */ + Busy, + /** Playing in sync: drift builds up slowly. */ + InSync, + /** Nothing to follow until the room or the player changes. */ + Idle, + } - private fun onPresence(all: Map) { - if (_role.value == null) return - val live = all.values.filter { !it.left } - _participants.value = live - .sortedBy { it.clientId } - .map { JamParticipant(it.clientId, it.displayName, it.isHost) } - - // Host-side: auto-kick banned participants that rejoin. - if (_role.value == JamRole.Host) { - live.filter { it.clientId in bannedClientIds }.forEach { banned -> - scope.launch { kickParticipant(banned.clientId, "banned by host") } + private suspend fun onAnchor(next: JamAnchor) { + anchor = next + roomVersion.value++ + // Play/pause is room-wide: follow every change of the room's play state. The room + // state is recorded first, so the resulting local intent isn't sent back as a command. + if (next.playing != lastHostIsPlaying) { + lastHostIsPlaying = next.playing + if (next.playing != playIntent.value) { + runCatching { if (next.playing) audioPlayer.play() else audioPlayer.pause() } + .onFailure { log.w(it) { "Failed to follow the host's play state" } } } + } + next.source?.let { pinSource(next.trackKey, it) } + } + + private suspend fun applyQueue(queue: JamQueue) { + roomVersion.value++ + _shuffleEnabled.value = queue.shuffleEnabled + val items = queue.items.filter { it.trackId.isNotBlank() || it.url.isNotBlank() } + if (items == lastAppliedItems) return + val previous = lastAppliedItems + lastAppliedItems = items + + // The host only appended items (e.g. accepted suggestions): merge them in + // without disturbing playback. + val current = audioPlayerQueue.currentQueueEntryFlow.value + if (previous.isNotEmpty() && items.size > previous.size && + items.take(previous.size) == previous && current != null + ) { + runCatching { audioPlayerQueue.addAllToQueue(items.drop(previous.size).map { it.toQueueEntry() }) } + .onFailure { log.w(it) { "Failed to append jam queue items" } } return } - val host = live.firstOrNull { it.isHost } - if (host != null) return + val room = anchor + val startIndex = room?.let { a -> items.indexOfFirst { it.key == a.trackKey } }?.takeIf { it >= 0 } + ?: items.indexOfFirst { it.key == current?.jamKey() }.takeIf { it >= 0 } + ?: 0 + holdSync(TRACK_CHANGE_HOLD_MS) + runCatching { + audioPlayerQueue.load( + entries = items.map { it.toQueueEntry() }, + autoPlay = room?.playing ?: false, + startPosition = startIndex) + }.onFailure { log.w(it) { "Failed to apply jam queue" } } + } - // Host left: the lowest client id takes over (deterministic, clock-free). - val candidate = live.minByOrNull { it.clientId } ?: return - if (candidate.clientId != localClientId) return + /** One step of following the room: right track, then right position. */ + private suspend fun syncOnce(): SyncStatus { + if (_role.value != JamRole.Guest || leaving) return SyncStatus.Idle + val room = anchor ?: return SyncStatus.Idle + val now = now() + if (now < holdSyncUntilMs) return SyncStatus.Busy - scope.launch { - delay(HOST_TAKEOVER_DELAY_MS) - val stillNoHost = jamClient.presence.value.values.none { !it.left && it.isHost } - if (!stillNoHost || leaving || _role.value != JamRole.Guest) return@launch - log.i { "Taking over as jam host (previous host left)" } - _role.value = JamRole.Host - jamClient.claimHost() - startHostBroadcast() + val current = audioPlayerQueue.currentQueueEntryFlow.value + if (current?.jamKey() != room.trackKey) { + val index = audioPlayerQueue.queueFlow.value.indexOfFirst { it.jamKey() == room.trackKey } + if (index < 0) return SyncStatus.Idle // The queue with this track hasn't arrived yet. + log.d { "Jam sync: switching to queue index $index" } + holdSync(TRACK_CHANGE_HOLD_MS) + corrector.reset() + audioPlayerQueue.jumpTo(index, autoPlay = room.playing) + return SyncStatus.Busy + } + + val expected = room.positionAt(jamClient.clock.serverTime(now)) + val actual = localPositionMs() + + if (!room.playing) { + // Paused room: line up once, so everyone resumes from the same spot. + if (abs(expected - actual) > PAUSED_ALIGN_THRESHOLD_MS) { + seek(expected) + return SyncStatus.Busy + } + return SyncStatus.Idle + } + // A buffering host isn't moving; guests wait for its next anchor instead of + // chasing a position that is about to be corrected. A buffering guest wakes + // up when its player starts playing. + if (!room.advancing || audioPlayer.playerStateFlow.value != PlayerState.PLAYING) return SyncStatus.Idle + val durationMs = audioPlayer.durationFlow.value.inWholeMilliseconds + if (expected < 0 || (durationMs > 0 && expected >= durationMs - END_GUARD_MS)) return SyncStatus.Busy + + val drift = expected - actual + if (lastCorrectionWasSeek) { + // Whatever is left after a seek is how far off the lead was. + seekLeadMs = (seekLeadMs + drift / 2).coerceIn(0L, MAX_SEEK_LEAD_MS) + lastCorrectionWasSeek = false + } + + return when (val correction = corrector.correct(expected, actual, room.rate, seekLeadMs)) { + JamCorrection.None -> { + // Exactly the room's rate (normally 1x): any other rate makes the player + // time-stretch, which costs CPU for as long as it lasts. + setRate(room.rate) + SyncStatus.InSync + } + is JamCorrection.Nudge -> { + setRate(correction.rate) + SyncStatus.Busy + } + is JamCorrection.Seek -> { + log.d { "Jam sync: ${drift}ms off, seeking (lead ${seekLeadMs}ms)" } + setRate(room.rate) + seek(correction.positionMs) + holdSync(SEEK_HOLD_MS) + lastCorrectionWasSeek = true + SyncStatus.Busy + } + } + } + + /** + * Uses the host's audio source for [trackKey] when we use the same audio plugin, so + * both sides stream the same recording. Uploads of one song differ in intro length, + * which no amount of clock sync can fix. + */ + private suspend fun pinSource(trackKey: String, tagged: PluginTaggedAudioSource) { + if (!pinnedSources.add("$trackKey|${tagged.source.id}")) return + val plugin = audioPluginSource.selectedAudioPlugin.value ?: return + if (plugin.pluginId != tagged.pluginId) return + val entry = audioPlayerQueue.queueFlow.value + .firstOrNull { it is QueueEntry.StreamingTrack && it.track.id == trackKey } as? QueueEntry.StreamingTrack + ?: return + val mine = runCatching { trackSources.getTrackSource(entry.track, plugin.pluginId) }.getOrNull() + if (mine == tagged.source) return + + log.i { "Jam: using the host's source ${tagged.source.id} for $trackKey" } + runCatching { + trackSources.saveTrackSource(entry.track, tagged.source, plugin.pluginId) + streamingUrlRepository.invalidateCachedStreamUrl(trackKey) + if (audioPlayerQueue.currentQueueEntryFlow.value?.jamKey() == trackKey) { + syncMutex.withLock { + holdSync(TRACK_CHANGE_HOLD_MS) + audioPlayerQueue.reloadCurrent() + } + } + }.onFailure { log.w(it) { "Failed to switch to the host's audio source" } } + } + + private suspend fun setRate(rate: Float) { + val rounded = (rate * 200).toInt() / 200f + if (appliedRate == rounded) return + // Only while playing: on iOS a non-zero rate also starts playback. + if (audioPlayer.playerStateFlow.value != PlayerState.PLAYING) return + runCatching { audioPlayer.setPlaybackSpeed(rounded) } + .onSuccess { appliedRate = rounded } + } + + private suspend fun restoreSpeed() { + if (appliedRate == null) return + appliedRate = null + val speed = speedBeforeJam ?: 1f + runCatching { audioPlayer.setPlaybackSpeed(speed) } + } + + private fun holdSync(durationMs: Long) { + holdSyncUntilMs = maxOf(holdSyncUntilMs, now() + durationMs) + } + + // ---------- Position tracking ---------- + + /** The player's position now, extrapolated from its last (coarse) report. */ + private fun localPositionMs(): Long { + if (!positionAdvancing) return positionSampleMs + val elapsed = now() - positionSampledAtMs + return positionSampleMs + (elapsed * audioPlayer.playbackSpeedFlow.value).toLong() + } + + private suspend fun seek(positionMs: Long) { + ownSeekUntilMs = now() + OWN_SEEK_WINDOW_MS + audioPlayer.seekTo(positionMs.coerceAtLeast(0).milliseconds) + positionSampleMs = positionMs + positionSampledAtMs = now() + } + + private fun onPosition(positionMs: Long) { + val now = now() + // A seek moves the position away from both the last report and the extrapolation; + // a stall (or a long pause) only from the extrapolation. + val jumped = abs(positionMs - positionSampleMs) > USER_SEEK_THRESHOLD_MS && + abs(positionMs - localPositionMs()) > USER_SEEK_THRESHOLD_MS && + now > ownSeekUntilMs && + now - entryChangedAtMs > TRACK_CHANGE_HOLD_MS + positionSampleMs = positionMs + positionSampledAtMs = now + if (!jumped || _roomCode.value == null || leaving) return + + // The user seeked: it applies to the whole room. + when (_role.value) { + JamRole.Host -> scope.launch { publishAnchor() } + JamRole.Guest -> { + holdSync(SEEK_HOLD_MS * 2) + jamClient.sendCommand(JamCommand.SeekTo(positionMs)) + } + null -> Unit } } // ---------- Helpers ---------- - private fun publishCommand(command: PlaybackCmd) { - scope.launch { - jamClient.publishCommand(JamMessage.PlaybackCommand(command)) - } - } + private fun serverUrl(): String? = + settingsRepository.userSettings.value.jamServerUrl.trim().ifBlank { null } private fun participantName(fallbackPrefix: String): String { val configured = settingsRepository.userSettings.value.jamParticipantName return configured.ifBlank { "$fallbackPrefix-${Random.nextInt(1000, 9999)}" } } - private fun newClientId(broker: org.spotube.spotube.modules.settings.JamBroker): String { - val suffix = buildString(6) { - val chars = "0123456789abcdef" - repeat(6) { append(chars[Random.nextInt(chars.length)]) } - } - return "${broker.clientIdPrefix.ifBlank { "spotube" }}-$suffix" + private fun newClientId(): String = buildString { + append("sp-") + val chars = "0123456789abcdef" + repeat(12) { append(chars[Random.nextInt(chars.length)]) } } private fun persistLastCode(code: String) { @@ -525,6 +783,11 @@ class JamRoomService( } } + private fun QueueEntry.jamKey(): String = when (this) { + is QueueEntry.StreamingTrack -> track.id + is QueueEntry.LocalTrack -> "local:$url" + } + private fun JamMediaItem.toQueueEntry(): QueueEntry = when { trackId.isNotBlank() -> QueueEntry.StreamingTrack( track = MetadataTrack( @@ -560,31 +823,27 @@ class JamRoomService( addedBy = addedBy) } - private fun JamMediaItem.matchesEntry(entry: QueueEntry?): Boolean { - if (entry == null) return false - return when (entry) { - is QueueEntry.StreamingTrack -> - trackId.isNotBlank() && entry.track.id == trackId - - is QueueEntry.LocalTrack -> - url.isNotBlank() && entry.url == url && entry.name == title - } - } - - private fun QueueEntry.matchesEntry(other: QueueEntry): Boolean = when { - this is QueueEntry.StreamingTrack && other is QueueEntry.StreamingTrack -> - this.track.id == other.track.id - - this is QueueEntry.LocalTrack && other is QueueEntry.LocalTrack -> - this.url == other.url && this.name == other.name - - else -> false - } - private fun now(): Long = Clock.System.now().toEpochMilliseconds() companion object { - private const val HOST_TAKEOVER_DELAY_MS = 1_500L - private const val AUTO_ADVANCE_WINDOW_MS = 2_000L + private const val CREATE_ATTEMPTS = 3 + private const val HOST_DRIFT_CHECK_INTERVAL_MS = 2_000L + private const val HOST_DRIFT_WINDOW = 5 + /** Below the guests' dead band, so the host's own error never adds up to a correction. */ + private const val HOST_REANCHOR_DRIFT_MS = 80L + private const val HOST_REANCHOR_MIN_INTERVAL_MS = 10_000L + /** A safety net only (e.g. a lost frame); costs one message a minute. */ + private const val HOST_ANCHOR_HEARTBEAT_MS = 60_000L + private const val SYNC_INTERVAL_MS = 500L + private const val IDLE_RECHECK_MS = 30_000L + private const val INITIAL_SEEK_LEAD_MS = 300L + private const val MAX_SEEK_LEAD_MS = 3_000L + private const val SEEK_HOLD_MS = 1_500L + private const val TRACK_CHANGE_HOLD_MS = 2_000L + private const val PAUSED_ALIGN_THRESHOLD_MS = 1_500L + private const val USER_SEEK_THRESHOLD_MS = 2_500L + private const val OWN_SEEK_WINDOW_MS = 1_500L + /** Near a track's end the next anchor (new track) is due; don't seek into the outro. */ + private const val END_GUARD_MS = 1_000L } } diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/remote/RemotePlaybackController.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/remote/RemotePlaybackController.kt index 3b37ff81..5ea612a6 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/remote/RemotePlaybackController.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/remote/RemotePlaybackController.kt @@ -202,7 +202,7 @@ class RemotePlaybackController( /** * Adds a single track to the active jam queue. The host applies it to the - * local (shared) queue directly; a guest suggests it to the host over MQTT. + * local (shared) queue directly; a guest suggests it to the host through the room server. */ fun addTrackToJam(track: MetadataTrack) { if (jamRoomService.role.value == null) return diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamScreen.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamScreen.kt index 568a018b..d9964f27 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamScreen.kt @@ -139,7 +139,7 @@ fun JamScreen(navigationCommands: NavigationCommands) { viewModel::joinSession ) } - BrokerStatus(state) { navigationCommands.navigateTo(Routes.JamSettings) } + ServerStatus(state) { navigationCommands.navigateTo(Routes.JamSettings) } } } } @@ -461,16 +461,16 @@ internal fun SessionPanel( } @Composable -internal fun BrokerStatus(state: JamUiState, onConfigure: () -> Unit) { +internal fun ServerStatus(state: JamUiState, onConfigure: () -> Unit) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - if (state.brokerConfigured) stringResource( + if (state.serverConfigured) stringResource( Res.string.jam_connection_host, - state.brokerHost + state.serverHost ) else stringResource(Res.string.jam_connection_missing), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant, diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamViewModel.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamViewModel.kt index 5041e371..42fc524a 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/jam/JamViewModel.kt @@ -57,8 +57,8 @@ data class JamUiState( val role: JamRole? = null, val participants: List = emptyList(), val roomCode: String? = null, - val brokerHost: String = "", - val brokerConfigured: Boolean = false, + val serverHost: String = "", + val serverConfigured: Boolean = false, val error: String? = null, val setupMode: JamSetupMode = JamSetupMode.Create, val codeInput: TextFieldState = TextFieldState(), @@ -115,7 +115,7 @@ class JamViewModel( val settings = values[5] as? org.spotube.spotube.modules.settings.UserSettings val localError = values[6] as String? val busy = values[8] as Boolean - val brokerConfigured = !settings?.jamBroker?.host.isNullOrBlank() + val serverConfigured = !settings?.jamServerUrl.isNullOrBlank() JamUiState( isActive = role != null, @@ -132,16 +132,17 @@ class JamViewModel( ) }, roomCode = roomCode, - brokerHost = settings?.jamBroker?.host.orEmpty(), - brokerConfigured = brokerConfigured, + serverHost = settings?.jamServerUrl.orEmpty() + .substringAfter("://").substringBefore('/'), + serverConfigured = serverConfigured, error = localError ?: connectionError, setupMode = values[7] as JamSetupMode, isBusy = busy, isLeaving = values[12] as Boolean, codeCopied = values[9] as Boolean, codeInput = codeInput, - canCreate = brokerConfigured, - canJoin = brokerConfigured && + canCreate = serverConfigured, + canJoin = serverConfigured && JamRoomCode.isValid(JamRoomCode.normalize(values[10] as String)), isHost = role == JamRole.Host, supportsNativeShare = supportsNativeShare diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsScreen.kt index 0643cb4b..5ac5953d 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsScreen.kt @@ -18,46 +18,30 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.spotube.spotube.core.ui.component.ApplicationMainBar -import org.spotube.spotube.modules.settings.components.SwitchSettingCard import org.spotube.spotube.modules.settings.components.TextInputSettingCard import org.spotube.spotube.modules.settings.sections.SettingsItemIcon import org.spotube.spotube.modules.settings.sections.settingsSectionCard import org.spotube.spotube.modules.settings.sections.settingsSectionHeader import org.spotube.spotube.resources.iconsax.CustomServer import org.spotube.spotube.resources.iconsax.Iconsax -import org.spotube.spotube.resources.iconsax.IconsaxEye import org.spotube.spotube.resources.iconsax.IconsaxInformation -import org.spotube.spotube.resources.iconsax.IconsaxTag -import org.spotube.spotube.resources.iconsax.IconsaxWifiSquare -import org.spotube.spotube.resources.iconsax.User import org.spotube.spotube.modules.settings.components.SettingCardItem import org.spotube.spotube.modules.shell.LocalAppShellBottomInset import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import spotube.composeapp.generated.resources.Res -import spotube.composeapp.generated.resources.settings_error_port_range -import spotube.composeapp.generated.resources.settings_error_whole_number -import spotube.composeapp.generated.resources.settings_jam_broker_client_id -import spotube.composeapp.generated.resources.settings_jam_broker_client_id_subtitle -import spotube.composeapp.generated.resources.settings_jam_broker_diagnostics_subtitle -import spotube.composeapp.generated.resources.settings_jam_broker_host -import spotube.composeapp.generated.resources.settings_jam_broker_password -import spotube.composeapp.generated.resources.settings_jam_broker_placeholder_note -import spotube.composeapp.generated.resources.settings_jam_broker_port -import spotube.composeapp.generated.resources.settings_jam_broker_test -import spotube.composeapp.generated.resources.settings_jam_broker_testing -import spotube.composeapp.generated.resources.settings_jam_broker_tls -import spotube.composeapp.generated.resources.settings_jam_broker_tls_subtitle -import spotube.composeapp.generated.resources.settings_jam_section_connection -import spotube.composeapp.generated.resources.settings_jam_section_credentials -import spotube.composeapp.generated.resources.settings_jam_section_diagnostics -import spotube.composeapp.generated.resources.settings_jam_section_identity -import spotube.composeapp.generated.resources.settings_jam_broker_username -import spotube.composeapp.generated.resources.settings_section_jam -import spotube.composeapp.generated.resources.settings_jam_broker_content_description -import spotube.composeapp.generated.resources.settings_jam_broker_username_placeholder -import spotube.composeapp.generated.resources.settings_jam_broker_test_fail import spotube.composeapp.generated.resources.jam_error_connection_failed +import spotube.composeapp.generated.resources.settings_jam_section_connection +import spotube.composeapp.generated.resources.settings_jam_section_diagnostics +import spotube.composeapp.generated.resources.settings_jam_server_diagnostics_subtitle +import spotube.composeapp.generated.resources.settings_jam_server_note +import spotube.composeapp.generated.resources.settings_jam_server_test +import spotube.composeapp.generated.resources.settings_jam_server_test_fail +import spotube.composeapp.generated.resources.settings_jam_server_testing +import spotube.composeapp.generated.resources.settings_jam_server_title +import spotube.composeapp.generated.resources.settings_jam_server_url +import spotube.composeapp.generated.resources.settings_jam_server_url_invalid +import spotube.composeapp.generated.resources.settings_section_jam @Composable fun JamSettingsScreen( @@ -80,7 +64,6 @@ internal fun JamSettingsContent( viewModel: JamSettingsViewModel = koinViewModel(), modifier: Modifier = Modifier) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val broker = uiState.settings.jamBroker val shellBottomInset = LocalAppShellBottomInset.current val contentPadding = remember(shellBottomInset) { PaddingValues(top = 16.dp, bottom = 16.dp + shellBottomInset) @@ -98,162 +81,58 @@ internal fun JamSettingsContent( settingsSectionCard( items = listOf( { + val invalidUrl = stringResource(Res.string.settings_jam_server_url_invalid) TextInputSettingCard( - title = stringResource(Res.string.settings_jam_broker_host), - value = broker.host, - placeholder = "broker.example.com", - icon = { SettingsItemIcon(Iconsax.CustomServer, stringResource(Res.string.settings_jam_broker_content_description)) }, - onValueSaved = { host -> - viewModel.update { - copy( - jamBroker = jamBroker.copy( - host = host - ) - ) - } - }) - }, - { - val errorWholeNumber = - stringResource(Res.string.settings_error_whole_number) - val errorPortRange = - stringResource(Res.string.settings_error_port_range) - - TextInputSettingCard( - title = stringResource(Res.string.settings_jam_broker_port), - value = broker.port.toString(), - placeholder = "1883", - normalize = { it.filter(Char::isDigit).take(5) }, + title = stringResource(Res.string.settings_jam_server_url), + value = uiState.settings.jamServerUrl, + placeholder = DEFAULT_JAM_SERVER_URL, validate = { value -> - val port = value.toIntOrNull() - when { - port == null -> errorWholeNumber - port !in 1..65535 -> errorPortRange - else -> null - } - }, - onValueSaved = { port -> - viewModel.update { copy(jamBroker = jamBroker.copy(port = port.toInt())) } + if (value.isBlank() || isJamServerUrl(value)) null else invalidUrl }, icon = { SettingsItemIcon( Iconsax.CustomServer, - stringResource(Res.string.settings_jam_broker_port) - ) - }) - }, - { - SwitchSettingCard( - title = stringResource(Res.string.settings_jam_broker_tls), - subtitle = stringResource(Res.string.settings_jam_broker_tls_subtitle), - checked = broker.useTls, - onCheckedChange = { tls -> - viewModel.update { copy(jamBroker = jamBroker.copy(useTls = tls)) } + stringResource(Res.string.settings_jam_server_title)) }, - icon = { - SettingsItemIcon( - Iconsax.IconsaxWifiSquare, - stringResource(Res.string.settings_jam_broker_tls) - ) - }) - })) - settingsSectionHeader(Res.string.settings_jam_section_credentials) - settingsSectionCard( - items = listOf( - { - TextInputSettingCard( - title = stringResource(Res.string.settings_jam_broker_username), - value = broker.username.orEmpty(), - placeholder = stringResource(Res.string.settings_jam_broker_username_placeholder), - onValueSaved = { username -> - viewModel.update { - copy(jamBroker = jamBroker.copy(username = username.ifBlank { null })) - } - }, - icon = { - SettingsItemIcon( - Iconsax.User, - stringResource(Res.string.settings_jam_broker_username) - ) - }) - }, - { - TextInputSettingCard( - title = stringResource(Res.string.settings_jam_broker_password), - value = broker.password.orEmpty(), - placeholder = "••••••••", - onValueSaved = { password -> - viewModel.update { - copy(jamBroker = jamBroker.copy(password = password.ifBlank { null })) - } - }, - icon = { - SettingsItemIcon( - Iconsax.IconsaxEye, - stringResource(Res.string.settings_jam_broker_password) - ) - }) - })) - settingsSectionHeader(Res.string.settings_jam_section_identity) - settingsSectionCard( - items = listOf( - { - TextInputSettingCard( - title = stringResource(Res.string.settings_jam_broker_client_id), - subtitle = stringResource(Res.string.settings_jam_broker_client_id_subtitle), - value = broker.clientIdPrefix, - placeholder = "spotube", - onValueSaved = { prefix -> - viewModel.update { - copy(jamBroker = jamBroker.copy(clientIdPrefix = prefix.ifBlank { "spotube" })) - } - }, - icon = { - SettingsItemIcon( - Iconsax.IconsaxTag, - stringResource(Res.string.settings_jam_broker_client_id) - ) - }) + onValueSaved = viewModel::setServerUrl) })) settingsSectionHeader(Res.string.settings_jam_section_diagnostics) settingsSectionCard( items = listOf( { - val canTest = broker.host.isNotBlank() && !uiState.isTesting val resultMessage = uiState.testResult?.fold( onSuccess = { it }, onFailure = { stringResource( - Res.string.settings_jam_broker_test_fail, + Res.string.settings_jam_server_test_fail, it.message ?: stringResource(Res.string.jam_error_connection_failed)) }) SettingCardItem( - enabled = canTest, - title = stringResource(Res.string.settings_jam_broker_test), + enabled = !uiState.isTesting, + title = stringResource(Res.string.settings_jam_server_test), subtitle = when { - uiState.isTesting -> stringResource(Res.string.settings_jam_broker_testing) + uiState.isTesting -> stringResource(Res.string.settings_jam_server_testing) resultMessage != null -> resultMessage - else -> stringResource(Res.string.settings_jam_broker_diagnostics_subtitle) + else -> stringResource(Res.string.settings_jam_server_diagnostics_subtitle) }, icon = { SettingsItemIcon( Iconsax.IconsaxInformation, - stringResource(Res.string.settings_jam_broker_test)) + stringResource(Res.string.settings_jam_server_test)) }, trailingContent = { MillenniumButton( onClick = viewModel::testConnection, - enabled = broker.host.isNotBlank(), loading = uiState.isTesting) { - Text(stringResource(Res.string.settings_jam_broker_test)) + Text(stringResource(Res.string.settings_jam_server_test)) } }, onClick = viewModel::testConnection) })) item { Text( - text = stringResource(Res.string.settings_jam_broker_placeholder_note), + text = stringResource(Res.string.settings_jam_server_note), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp)) diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsViewModel.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsViewModel.kt index 61e58722..2cfc3ad7 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/JamSettingsViewModel.kt @@ -28,9 +28,12 @@ class JamSettingsViewModel( } } - fun update(transform: UserSettings.() -> UserSettings) { + /** A blank URL goes back to the public server. */ + fun setServerUrl(url: String) { viewModelScope.launch { - settingsRepository.updateSettings(uiState.value.settings.transform()) + val serverUrl = url.trim().trimEnd('/').ifBlank { DEFAULT_JAM_SERVER_URL } + settingsRepository.updateSettings(uiState.value.settings.copy(jamServerUrl = serverUrl)) + _uiState.update { it.copy(testResult = null) } } } @@ -39,7 +42,7 @@ class JamSettingsViewModel( _uiState.update { it.copy(isTesting = true, testResult = null) } viewModelScope.launch { try { - val result = jamRoomClient.testConnection(uiState.value.settings.jamBroker) + val result = jamRoomClient.testConnection(uiState.value.settings.jamServerUrl) _uiState.update { it.copy(testResult = result) } } finally { _uiState.update { it.copy(isTesting = false) } diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsModels.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsModels.kt index 90f87e3e..8b7aca13 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsModels.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsModels.kt @@ -59,9 +59,10 @@ data class UserSettings( val remoteControlDeviceName: String = "", val remoteControlDeviceId: String = "", - // Group Jam (MQTT) + // Group Jam val jamParticipantName: String = "", - val jamBroker: JamBroker = JamBroker(), + /** Room server (`jam-server/`); self-hosters point this at their own deployment. */ + val jamServerUrl: String = DEFAULT_JAM_SERVER_URL, val lastJamCode: String = "", // Downloads @@ -91,21 +92,12 @@ data class UserSettings( val supportRemindersEnabled: Boolean = true, val lastSupportReminderEpochMs: Long = 0L) -/** - * Configuration for the MQTT broker used by Group Jam. The host is a placeholder - * until a real broker is configured; users can self-host and point the app at it. - */ -@Serializable -data class JamBroker( - val name: String = "", - val host: String = "broker.hivemq.com", - val port: Int = 8883, - val useTls: Boolean = true, - val username: String? = null, - val password: String? = null, - val clientIdPrefix: String = "spotube", - val keepAliveSeconds: Int = 30, - val connectionTimeoutSeconds: Int = 10) +/** The public Group Jam room server. */ +const val DEFAULT_JAM_SERVER_URL = "https://spotube-jam.workers.dev" + +/** Whether [url] can be a room server address (http(s), or ws(s) for self-hosters). */ +internal fun isJamServerUrl(url: String): Boolean = + Regex("^(https?|wss?)://[^\\s/?#]+(/\\S*)?$", RegexOption.IGNORE_CASE).matches(url.trim()) /** * opus/webm wherever the platform's player can open WebM; iOS (AVPlayer) can't, so it streams AAC. diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsRepository.kt index b8b3772c..0c4f62c1 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/SettingsRepository.kt @@ -34,7 +34,12 @@ interface UserSettingsSource { val userSettings: StateFlow } -class SettingsRepository(private val database: Database) : UserSettingsSource { +/** [UserSettingsSource] that can also save, for consumers that update a setting or two. */ +interface UserSettingsStore : UserSettingsSource { + suspend fun updateSettings(newSettings: UserSettings) +} + +class SettingsRepository(private val database: Database) : UserSettingsStore { companion object { private val SETTINGS_KEY = stringPreferencesKey("user_settings") @@ -64,7 +69,7 @@ class SettingsRepository(private val database: Database) : UserSettingsSource { initialValue = UserSettings() // Default value ) - suspend fun updateSettings(newSettings: UserSettings) { + override suspend fun updateSettings(newSettings: UserSettings) { database.settingsDataStore.edit { prefs -> prefs[SETTINGS_KEY] = settingsJson.encodeToString(newSettings) } diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/sections/JamSection.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/sections/JamSection.kt index 53192bff..fcbfff82 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/sections/JamSection.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/modules/settings/sections/JamSection.kt @@ -24,8 +24,7 @@ import org.spotube.spotube.resources.iconsax.CustomServer import org.spotube.spotube.resources.iconsax.Iconsax import org.jetbrains.compose.resources.stringResource import spotube.composeapp.generated.resources.Res -import spotube.composeapp.generated.resources.settings_jam_broker_host_subtitle -import spotube.composeapp.generated.resources.settings_jam_broker_title +import spotube.composeapp.generated.resources.settings_jam_server_title import spotube.composeapp.generated.resources.settings_section_jam internal fun LazyListScope.jamSection( @@ -36,20 +35,17 @@ internal fun LazyListScope.jamSection( items = listOf( { SettingCardItem( - title = stringResource(Res.string.settings_jam_broker_title), - subtitle = stringResource( - Res.string.settings_jam_broker_host_subtitle, - settings.jamBroker.host.ifBlank { "—" }, - settings.jamBroker.port), + title = stringResource(Res.string.settings_jam_server_title), + subtitle = settings.jamServerUrl, icon = { SettingsItemIcon( Iconsax.CustomServer, - stringResource(Res.string.settings_jam_broker_title)) + stringResource(Res.string.settings_jam_server_title)) }, trailingContent = { MillenniumIcon( imageVector = FeatherIcons.ChevronRight, - contentDescription = stringResource(Res.string.settings_jam_broker_title), + contentDescription = stringResource(Res.string.settings_jam_server_title), tint = AppTheme.colors.onSurfaceVariant) }, onClick = { navigationCommands.navigateTo(Routes.JamSettings) }) diff --git a/composeApp/src/commonTest/kotlin/org/spotube/spotube/core/jam/JamSyncTest.kt b/composeApp/src/commonTest/kotlin/org/spotube/spotube/core/jam/JamSyncTest.kt new file mode 100644 index 00000000..914ea50a --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/spotube/spotube/core/jam/JamSyncTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.spotube.spotube.core.jam + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class JamSyncTest { + + // --- JamClock --- + + @Test + fun clockIsUnknownBeforeTheFirstSample() { + val clock = JamClock() + assertNull(clock.offsetMs) + assertEquals(1_000L, clock.serverTime(1_000L)) + } + + @Test + fun clockAssumesSymmetricLatency() { + val clock = JamClock() + // Server is 5 s ahead; 40 ms each way. + clock.addSample(clientSentMs = 1_000, serverMs = 6_040, clientReceivedMs = 1_080) + assertEquals(5_000L, clock.offsetMs) + assertEquals(80L, clock.rttMs) + } + + @Test + fun clockTrustsTheFastestRoundTrip() { + val clock = JamClock() + clock.addSample(clientSentMs = 0, serverMs = 5_030, clientReceivedMs = 60) // offset 5000 + // A slow sample with a lopsided path skews the estimate; it must not win. + clock.addSample(clientSentMs = 1_000, serverMs = 6_400, clientReceivedMs = 1_500) // offset 5150 + assertEquals(5_000L, clock.offsetMs) + } + + @Test + fun clockForgetsSamplesOutsideTheWindow() { + val clock = JamClock(window = 2) + clock.addSample(0, 5_010, 20) // fast, offset 5000 + clock.addSample(100, 5_250, 300) // offset 5050 + clock.addSample(400, 5_660, 600) // offset 5160 — the fast one is now gone + assertEquals(5_050L, clock.offsetMs) + } + + @Test + fun clockIgnoresNegativeRoundTrips() { + val clock = JamClock() + clock.addSample(clientSentMs = 100, serverMs = 0, clientReceivedMs = 50) + assertNull(clock.offsetMs) + } + + // --- JamAnchor --- + + @Test + fun anchorExtrapolatesWhileAdvancing() { + val anchor = JamAnchor("t", 0, positionMs = 10_000, serverTimeMs = 50_000, playing = true, rate = 1.5f) + assertEquals(13_000L, anchor.positionAt(52_000)) + } + + @Test + fun anchorHoldsStillWhenNotAdvancing() { + val paused = JamAnchor("t", 0, positionMs = 10_000, serverTimeMs = 50_000, playing = false) + assertEquals(10_000L, paused.positionAt(60_000)) + val buffering = JamAnchor("t", 0, 10_000, 50_000, playing = true, advancing = false) + assertEquals(10_000L, buffering.positionAt(60_000)) + } + + // --- JamDriftCorrector --- + + @Test + fun smallDriftIsLeftAlone() { + val corrector = JamDriftCorrector() + assertEquals(JamCorrection.None, corrector.correct(10_080, 10_000, 1f, seekLeadMs = 300)) + } + + @Test + fun behindSpeedsUpAndAheadSlowsDown() { + val behind = JamDriftCorrector().correct(10_600, 10_000, 1f, seekLeadMs = 300) + assertIs(behind) + assertTrue(behind.rate > 1f && behind.rate <= 1.05f) + + val ahead = JamDriftCorrector().correct(10_000, 10_600, 1f, seekLeadMs = 300) + assertIs(ahead) + assertTrue(ahead.rate < 1f && ahead.rate >= 0.95f) + } + + @Test + fun nudgeFollowsTheRoomRate() { + val nudge = JamDriftCorrector().correct(10_300, 10_000, 1.25f, seekLeadMs = 0) + assertIs(nudge) + assertEquals(1.25f * 1.05f, nudge.rate, 0.001f) + } + + @Test + fun largeDriftSeeksAheadByTheLead() { + val seek = JamDriftCorrector().correct(20_000, 10_000, 1f, seekLeadMs = 400) + assertEquals(JamCorrection.Seek(20_400), seek) + } + + @Test + fun aSingleNoisyReadingDoesNotSeek() { + val corrector = JamDriftCorrector(seekThresholdMs = 1_500) + repeat(4) { corrector.correct(10_000, 10_000, 1f, 0) } + // One coarse report 2 s off (under the immediate-seek bar) is outvoted by the median. + assertEquals(JamCorrection.None, corrector.correct(12_000, 10_000, 1f, 0)) + } + + @Test + fun persistentDriftSeeksOnceTheMedianAgrees() { + val corrector = JamDriftCorrector(seekThresholdMs = 1_500) + val results = List(3) { corrector.correct(12_000, 10_000, 1f, 0) } + assertIs(results[0]) + assertIs(results[1]) + assertEquals(JamCorrection.Seek(12_000), results[2]) + } +} diff --git a/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamRoomClientIntegrationTest.kt b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamRoomClientIntegrationTest.kt new file mode 100644 index 00000000..dcc7ed86 --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamRoomClientIntegrationTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.spotube.spotube.core.jam + +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.spotube.spotube.core.network.HttpClientFactory +import kotlin.math.abs +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Runs the real client against a room server. Skipped unless `JAM_TEST_URL` is set: + * `cd jam-server && npm run dev`, then `JAM_TEST_URL=http://127.0.0.1:8787 ./gradlew :composeApp:jvmTest`. + */ +class JamRoomClientIntegrationTest { + private val serverUrl: String? = System.getenv("JAM_TEST_URL") + + @Test + fun hostAndGuestShareARoom() = runBlocking { + val url = serverUrl ?: return@runBlocking + val http = HttpClientFactory() + val host = JamRoomClient(http) + val guest = JamRoomClient(http) + val code = JamRoomCode.generate() + try { + host.testConnection(url).getOrThrow() + + val hostWelcome = host.connect(url, code, "it-host-1", "Host", create = true).getOrThrow() + assertEquals("it-host-1", hostWelcome.host) + // Same machine: the clock offset is just measurement noise. + assertTrue(abs(host.clock.offsetMs!!) < 50, "offset ${host.clock.offsetMs}") + + val queue = JamQueue(listOf(JamMediaItem(url = "", trackId = "t1", title = "Song", artist = "A", + album = "", durationMs = 1_000, coverUrl = "", protocol = "PROGRESSIVE"))) + host.sendQueue(queue) + val anchor = JamAnchor("t1", 0, positionMs = 1_234, serverTimeMs = host.clock.serverTime(0), playing = true) + host.sendAnchor(anchor) + Thread.sleep(300) + + // Late joiners get the retained queue and anchor in the welcome. + val guestWelcome = guest.connect(url, code, "it-guest-1", "Guest", create = false).getOrThrow() + assertEquals(queue, guestWelcome.queue) + assertEquals(anchor, guestWelcome.anchor) + assertEquals(listOf("it-host-1", "it-guest-1"), guestWelcome.participants.map { it.id }) + + val received = async(start = CoroutineStart.UNDISPATCHED) { host.events.filterIsInstance().first() } + guest.sendCommand(JamCommand.SeekTo(42_000)) + val command = withTimeout(5_000) { received.await() } + assertEquals("it-guest-1", command.from) + assertEquals(JamCommand.SeekTo(42_000), command.command) + + val closed = async(start = CoroutineStart.UNDISPATCHED) { guest.events.filterIsInstance().first() } + host.kick("it-guest-1", ban = false) + assertEquals("kicked", withTimeout(5_000) { closed.await() }.code) + + val wrongRoom = guest.connect(url, JamRoomCode.generate(), "it-guest-1", "Guest", create = false) + assertEquals("room_not_found", (wrongRoom.exceptionOrNull() as JamRoomException).code) + } finally { + host.disconnect() + guest.disconnect() + http.close() + } + } +} diff --git a/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamSessionSimulationTest.kt b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamSessionSimulationTest.kt new file mode 100644 index 00000000..60c1a888 --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/JamSessionSimulationTest.kt @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.spotube.spotube.core.jam + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.spotube.plugin_interfaces.plugin_apis.audio.AudioSource +import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack +import org.spotube.spotube.core.audioplayer.AudioPlayerInterface +import org.spotube.spotube.core.audioplayer.AudioPlayerQueue +import org.spotube.spotube.core.audioplayer.LoopState +import org.spotube.spotube.core.audioplayer.MediaItem +import org.spotube.spotube.core.audioplayer.PlayerState +import org.spotube.spotube.core.audioplayer.QueueCollectionEntry +import org.spotube.spotube.core.audioplayer.QueueEntry +import org.spotube.spotube.core.network.HttpClientFactory +import org.spotube.spotube.core.server.FakeAudioPluginService +import org.spotube.spotube.core.server.FakeAudioPluginSource +import org.spotube.spotube.core.server.FakeStreamingAudioPlayerQueue +import org.spotube.spotube.core.server.FakeTrackSourceRepository +import org.spotube.spotube.core.server.FakeUserSettingsSource +import org.spotube.spotube.core.server.StreamingUrlRepository +import org.spotube.spotube.modules.settings.UserSettings +import org.spotube.spotube.modules.settings.UserSettingsStore +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.math.abs +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** + * A host and a guest [JamRoomService], each driving a simulated player, in one room on a + * real room server. The guest's player runs 1% fast, the host's 0.1% slow (so it has to + * re-anchor), and both report positions coarsely, like real players. Skipped unless `JAM_TEST_URL` is set (see [JamRoomClientIntegrationTest]). + */ +class JamSessionSimulationTest { + private val serverUrl: String? = System.getenv("JAM_TEST_URL") + + @BeforeTest + fun setup() { + startKoin { modules(module { factory { (tag: String?) -> Logger.withTag(tag ?: "test") } }) } + } + + @AfterTest + fun teardown() { + stopKoin() + } + + @Test + fun guestFollowsTheHost() = runBlocking { + val url = serverUrl ?: return@runBlocking + val http = HttpClientFactory() + val tracks = List(3) { track("track-$it") } + val hostSource = source("host-upload") + val host = Participant(http, url, speedError = 0.999) + val guest = Participant(http, url, speedError = 1.01) + host.sources.seed(tracks[0], hostSource, PLUGIN) + guest.sources.seed(tracks[0], source("other-upload"), PLUGIN) + + try { + host.queue.load(tracks.map { QueueEntry.StreamingTrack(it, url = "") }, autoPlay = true, startPosition = 0) + delay(2_000) + + val code = host.service.createRoom().getOrThrow() + guest.service.joinRoom(code).getOrThrow() + assertEquals(JamRole.Host, host.service.role.value) + assertEquals(JamRole.Guest, guest.service.role.value) + + delay(7_000) + assertInSync(host, guest, "after joining") + assertEquals(hostSource, guest.sources.rawEntryFor(tracks[0].id)?.source, "guest uses the host's source") + + host.player.seekTo(60_000.milliseconds) + delay(5_000) + assertInSync(host, guest, "after the host seeked") + + // A guest's seek moves the whole room. + guest.player.seekTo(90_000.milliseconds) + // Two hops (guest -> host -> guest) and two seeks: allow longer to settle. + delay(8_000) + assertTrue(host.player.truePosition() in 96_000..100_000, "host followed the guest's seek: ${host.player.truePosition()}") + assertInSync(host, guest, "after the guest seeked") + + guest.player.pause() + delay(1_500) + assertEquals(PlayerState.PAUSED, host.player.playerStateFlow.value, "guest paused the room") + + host.player.play() + host.service.skipNext() + delay(6_000) + assertEquals(1, host.player.index.value) + assertEquals(1, guest.player.index.value, "guest followed the skip") + assertEquals(PlayerState.PLAYING, guest.player.playerStateFlow.value) + assertInSync(host, guest, "after the skip") + + // Steady playback costs (almost) nothing: count the anchors a listener receives. + val listener = JamRoomClient(http) + try { + listener.connect(url, code, "it-listener", "Listener", create = false).getOrThrow() + delay(1_000) // The host answers the join with an anchor. + var anchors = 0 + val counting = launch { + listener.events.collect { if (it is JamRoomEvent.Anchor) anchors++ } + } + delay(STEADY_WINDOW_MS) + counting.cancel() + println("Jam simulation: $anchors anchor(s) in ${STEADY_WINDOW_MS / 1000}s of steady playback") + assertTrue(anchors <= 2, "steady playback sent $anchors anchors") + assertInSync(host, guest, "after steady playback") + } finally { + listener.disconnect() + } + } finally { + guest.close() + host.close() + http.close() + } + } + + /** Losing the network (guest, then host) must not crash, and the room picks up again. */ + @Test + fun roomSurvivesNetworkLoss() = runBlocking { + val url = serverUrl ?: return@runBlocking + val http = HttpClientFactory() + val hostNet = TcpProxy(url) + val guestNet = TcpProxy(url) + val tracks = List(3) { track("track-$it") } + val host = Participant(http, hostNet.url, speedError = 1.0) + val guest = Participant(http, guestNet.url, speedError = 1.01) + val crashes = CopyOnWriteArrayList() + val previousHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { _, e -> crashes += e } + + try { + host.queue.load(tracks.map { QueueEntry.StreamingTrack(it, url = "") }, autoPlay = true, startPosition = 0) + delay(1_000) + val code = host.service.createRoom().getOrThrow() + guest.service.joinRoom(code).getOrThrow() + delay(5_000) + assertInSync(host, guest, "before the guest lost its network") + + guestNet.goOffline() + delay(1_000) + assertEquals(false, guest.service.isConnected.value, "guest noticed the drop") + assertEquals(code, guest.service.roomCode.value, "guest stays in the room while offline") + // Meanwhile the room moves on without the guest. + host.player.seekTo(40_000.milliseconds) + delay(4_000) + assertEquals(PlayerState.PLAYING, guest.player.playerStateFlow.value, "guest keeps playing offline") + + guestNet.goOnline() + delay(10_000) // Up to one backoff step, then resync. + assertEquals(true, guest.service.isConnected.value, "guest reconnected") + assertEquals(JamRole.Guest, guest.service.role.value) + assertInSync(host, guest, "after the guest reconnected") + + // A host that drops for less than the grace period keeps the room. + hostNet.goOffline() + delay(5_000) + assertEquals(PlayerState.PLAYING, guest.player.playerStateFlow.value, "guest keeps playing while the host is away") + hostNet.goOnline() + delay(10_000) + assertEquals(true, host.service.isConnected.value, "host reconnected") + assertEquals(JamRole.Host, host.service.role.value, "host kept the room") + host.player.seekTo(100_000.milliseconds) + delay(5_000) + assertInSync(host, guest, "after the host reconnected and seeked") + + assertTrue(crashes.isEmpty(), "uncaught exceptions: $crashes") + } finally { + Thread.setDefaultUncaughtExceptionHandler(previousHandler) + guest.close() + host.close() + hostNet.close() + guestNet.close() + http.close() + } + } + + private fun assertInSync(host: Participant, guest: Participant, phase: String) { + assertEquals(host.player.index.value, guest.player.index.value, "same track $phase") + val gap = guest.player.truePosition() - host.player.truePosition() + println("Jam simulation: guest is ${gap}ms off $phase") + assertTrue(abs(gap) < MAX_GAP_MS, "guest is ${gap}ms off $phase") + } + + private class Participant(http: HttpClientFactory, serverUrl: String, speedError: Double) { + val player = SimPlayer(speedError) + val queue = SimQueue(player) + val sources = FakeTrackSourceRepository() + private val plugins = FakeAudioPluginSource(FakeAudioPluginService(PLUGIN)) + val service = JamRoomService( + jamClient = JamRoomClient(http), + audioPlayer = player, + audioPlayerQueue = queue, + settingsRepository = FakeSettingsStore(UserSettings(jamServerUrl = serverUrl)), + audioPluginSource = plugins, + trackSources = sources, + streamingUrlRepository = StreamingUrlRepository( + plugins, FakeStreamingAudioPlayerQueue(), sources, FakeUserSettingsSource())) + + suspend fun close() { + service.leaveRoom() + player.dispose() + } + } + + private class FakeSettingsStore(initial: UserSettings) : UserSettingsStore { + override val userSettings = MutableStateFlow(initial) + override suspend fun updateSettings(newSettings: UserSettings) { + userSettings.value = newSettings + } + } + + /** + * Plays a list of tracks in real time. Its clock runs [speedError] times true speed (as + * a drifting device would), positions are reported every 250 ms rounded to 100 ms, and + * every seek or track change buffers briefly. + */ + private class SimPlayer(private val speedError: Double) : AudioPlayerInterface { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private var durations: List = emptyList() + private var basePosition = 0L + private var baseAt = now() + private var speed = 1f + private var playWhenReady = false + + val index = MutableStateFlow(-1) + private val _state = MutableStateFlow(PlayerState.IDLE) + private val _position = MutableStateFlow(Duration.ZERO) + private val _duration = MutableStateFlow(Duration.ZERO) + private val _speed = MutableStateFlow(1f) + private val _completion = MutableSharedFlow(extraBufferCapacity = 4) + + init { + scope.launch { + while (isActive) { + delay(250) + val position = truePosition() + val duration = durations.getOrNull(index.value) ?: continue + if (position >= duration) { + _completion.tryEmit(Unit) + if (index.value < durations.lastIndex) startTrack(index.value + 1) + continue + } + _position.value = (position / 100 * 100).milliseconds + } + } + } + + @Synchronized + fun truePosition(): Long = + if (_state.value == PlayerState.PLAYING) { + basePosition + ((now() - baseAt) * speed * speedError).toLong() + } else { + basePosition + } + + @Synchronized + private fun rebase(position: Long = truePosition()) { + basePosition = position + baseAt = now() + } + + fun loadTracks(trackDurations: List, start: Int, autoPlay: Boolean) { + durations = trackDurations + playWhenReady = autoPlay + startTrack(start) + } + + fun startTrack(i: Int) { + index.value = i + _duration.value = durations[i].milliseconds + rebase(0) + _position.value = Duration.ZERO + buffer() + } + + private fun buffer() { + rebase() + _state.value = PlayerState.BUFFERING + scope.launch { + delay(BUFFER_MS) + rebase() + _state.value = if (playWhenReady) PlayerState.PLAYING else PlayerState.PAUSED + } + } + + override suspend fun play() { + playWhenReady = true + rebase() + if (_state.value != PlayerState.BUFFERING) _state.value = PlayerState.PLAYING + } + + override suspend fun pause() { + playWhenReady = false + rebase() + if (_state.value != PlayerState.BUFFERING) _state.value = PlayerState.PAUSED + } + + override suspend fun seekTo(position: Duration) { + rebase(position.inWholeMilliseconds) + _position.value = position + buffer() + } + + override suspend fun setPlaybackSpeed(speed: Float) { + rebase() + this.speed = speed + _speed.value = speed + } + + override suspend fun skipToNext() = startTrack((index.value + 1).coerceAtMost(durations.lastIndex)) + override suspend fun skipToPrevious() = startTrack((index.value - 1).coerceAtLeast(0)) + override suspend fun jumpTo(index: Int) = startTrack(index) + + override val playerStateFlow: StateFlow = _state + override val positionFlow: StateFlow = _position + override val durationFlow: StateFlow = _duration + override val playbackSpeedFlow: StateFlow = _speed + override val completionFlow: Flow = _completion + override val shuffleModeFlow: StateFlow = MutableStateFlow(false) + override val currentMediaItemFlow: StateFlow = MutableStateFlow(null) + override val playlistFlow: StateFlow> = MutableStateFlow(emptyList()) + override val bufferingPositionFlow: StateFlow = MutableStateFlow(Duration.ZERO) + override val loopStateFlow: StateFlow = MutableStateFlow(LoopState.NONE) + override val volumeFlow: StateFlow = MutableStateFlow(1f) + override val errorFlow: Flow = MutableSharedFlow() + + override suspend fun stop() = pause() + override suspend fun loop(state: LoopState) = Unit + override suspend fun shuffle(enabled: Boolean) = Unit + override suspend fun load(playlist: List, autoPlay: Boolean, startPosition: Int) = error("use SimQueue") + override suspend fun addMediaItem(mediaItem: MediaItem) = Unit + override suspend fun insertMediaItemAtNextIndex(mediaItem: MediaItem) = Unit + override suspend fun removeMediaItem(mediaItem: MediaItem) = Unit + override suspend fun moveMediaItem(fromIndex: Int, toIndex: Int) = Unit + override suspend fun setVolume(volume: Float) = Unit + override fun isDisposed(): Boolean = !scope.isActive + override fun dispose() = scope.cancel() + } + + private class SimQueue(private val player: SimPlayer) : AudioPlayerQueue { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val entries = MutableStateFlow>(emptyList()) + + override val queueFlow: StateFlow> = entries + override val currentQueueEntryFlow: StateFlow = + combine(entries, player.index) { list, i -> list.getOrNull(i) } + .stateIn(scope, SharingStarted.Eagerly, null) + override val currentCollectionEntryFlow: StateFlow = MutableStateFlow(null) + override val collectionHistoryFlow: StateFlow> = MutableStateFlow(emptyList()) + + override suspend fun load( + entries: List, + autoPlay: Boolean, + startPosition: Int, + collectionEntry: QueueCollectionEntry?) { + this.entries.value = entries + player.loadTracks(entries.map { (it as QueueEntry.StreamingTrack).track.durationMs }, startPosition, autoPlay) + } + + override suspend fun jumpTo(index: Int, autoPlay: Boolean) { + if (autoPlay) player.play() else player.pause() + player.startTrack(index) + } + + override suspend fun reloadCurrent() = jumpTo(player.index.value, autoPlay = true) + override suspend fun addToQueue(entry: QueueEntry) = addAllToQueue(listOf(entry), null) + override suspend fun addAllToQueue(entries: List, collectionEntry: QueueCollectionEntry?) { + this.entries.value += entries + } + + override suspend fun addAllAfterCurrent(entries: List) = addAllToQueue(entries, null) + override suspend fun removeFromQueue(entry: QueueEntry) = Unit + override suspend fun removeFromQueueByMediaUrl(mediaUrl: String) = Unit + override suspend fun move(fromIndex: Int, toIndex: Int) = Unit + override suspend fun clear() = Unit + override suspend fun getQueue(): List = entries.value + override suspend fun getCurrentQueueEntry(): QueueEntry? = currentQueueEntryFlow.value + override suspend fun getCurrentCollectionEntry(): QueueCollectionEntry? = null + override suspend fun getCollectionHistory(): List = emptyList() + } + + companion object { + private const val PLUGIN = "test-audio-plugin" + private const val BUFFER_MS = 200L + private const val MAX_GAP_MS = 200L + private const val STEADY_WINDOW_MS = 40_000L + + private fun now() = Clock.System.now().toEpochMilliseconds() + + private fun track(id: String) = MetadataTrack( + id = id, title = id, durationMs = 180_000, trackNumber = null, discNumber = null, + artists = emptyList(), album = null, thumbnails = null, explicit = null, + popularity = null, isrcCode = null, externalUri = null) + + private fun source(id: String) = AudioSource.Basic( + id = id, title = id, artist = null, album = null, thumbnails = emptyList(), + externalUri = null, confidence = 1f) + } +} diff --git a/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/TcpProxy.kt b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/TcpProxy.kt new file mode 100644 index 00000000..9eb8e2db --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/core/jam/TcpProxy.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.spotube.spotube.core.jam + +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import java.net.URI +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.concurrent.thread + +/** + * A TCP proxy in front of the room server that a test can cut, like a phone losing its + * network: [goOffline] resets every open connection and refuses new ones until [goOnline]. + */ +class TcpProxy(target: String) : AutoCloseable { + private val targetHost: String + private val targetPort: Int + private val server = ServerSocket().apply { + reuseAddress = true + bind(InetSocketAddress("127.0.0.1", 0)) + } + private val sockets = CopyOnWriteArrayList() + + @Volatile + private var online = true + + init { + val uri = URI(target) + targetHost = uri.host + targetPort = if (uri.port > 0) uri.port else 80 + thread(isDaemon = true, name = "tcp-proxy") { + while (!server.isClosed) { + val client = runCatching { server.accept() }.getOrNull() ?: break + if (!online) { + reset(client) + continue + } + val upstream = runCatching { Socket(targetHost, targetPort) }.getOrElse { + reset(client) + continue + } + sockets += client + sockets += upstream + pipe(client, upstream) + pipe(upstream, client) + } + } + } + + val url: String get() = "http://127.0.0.1:${server.localPort}" + + fun goOffline() { + online = false + sockets.forEach(::reset) + sockets.clear() + } + + fun goOnline() { + online = true + } + + override fun close() { + goOffline() + server.close() + } + + private fun pipe(from: Socket, to: Socket) = thread(isDaemon = true, name = "tcp-proxy-pipe") { + runCatching { from.getInputStream().copyTo(to.getOutputStream()) } + reset(from) + reset(to) + } + + /** Closes with a TCP reset, as an abrupt network loss looks to the other side. */ + private fun reset(socket: Socket) { + runCatching { + socket.setSoLinger(true, 0) + socket.close() + } + } +} diff --git a/composeApp/src/jvmTest/kotlin/org/spotube/spotube/modules/jam/JamLayoutTest.kt b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/modules/jam/JamLayoutTest.kt index 7a67d3bb..823248e9 100644 --- a/composeApp/src/jvmTest/kotlin/org/spotube/spotube/modules/jam/JamLayoutTest.kt +++ b/composeApp/src/jvmTest/kotlin/org/spotube/spotube/modules/jam/JamLayoutTest.kt @@ -65,20 +65,20 @@ class JamLayoutTest { assertEquals(100.dp, viewModel.compactChromeUiState.value.measuredBottomBarHeight) } - /** Actual Compose controls rendered without native-window capture or an MQTT connection. */ + /** Actual Compose controls rendered without native-window capture or a room server connection. */ @OptIn(ExperimentalComposeUiApi::class) @Test fun renderRoomControlsAtNarrowAndWideSizes() { val output = File("build/reports/jam-ui").apply { mkdirs() } val states = mapOf( "create" to JamUiState( - brokerConfigured = true, - brokerHost = "test.mosquitto.org", + serverConfigured = true, + serverHost = "jam.example.com", canCreate = true ), "join" to JamUiState( - brokerConfigured = true, - brokerHost = "test.mosquitto.org", + serverConfigured = true, + serverHost = "jam.example.com", setupMode = JamSetupMode.Join ), "host" to JamUiState( @@ -86,8 +86,8 @@ class JamLayoutTest { isConnected = true, isHost = true, roomCode = "RC5FJ8", - brokerConfigured = true, - brokerHost = "test.mosquitto.org", + serverConfigured = true, + serverHost = "jam.example.com", participants = listOf( JamParticipantUi("host", "Host-8316", "H", true, false), JamParticipantUi("guest", "Guest-6684", "G", false, true) @@ -97,8 +97,8 @@ class JamLayoutTest { isActive = true, isConnected = true, roomCode = "RC5FJ8", - brokerConfigured = true, - brokerHost = "test.mosquitto.org", + serverConfigured = true, + serverHost = "jam.example.com", participants = listOf(JamParticipantUi("host", "Host-8316", "H", true, false)) ) ) @@ -161,7 +161,7 @@ class JamLayoutTest { {}, {}) else SetupPanel(state, {}, {}, {}) - BrokerStatus(state, {}) + ServerStatus(state, {}) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8ec04eb..dbf032fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,8 +36,6 @@ kotlin = "2.4.21" kotlinx-coroutines = "1.11.0" kotlinx-serialization-json = "1.11.0" millennium = "0.1.1" -mqttClient = "2.1.1" -mqttBuffer = "6.30.8" multiplatformMarkdownRenderer = "0.45.0" murmurhash = "0.4.2" newpipeextractor = "v0.26.5" @@ -105,10 +103,6 @@ ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } millennium = { module = "org.spotube.millennium:millennium", version.ref = "millennium" } -mqtt-x-models = { module = "com.ditchoom:mqtt-5-models", version.ref = "mqttClient" } -mqtt-client = { module = "com.ditchoom:mqtt-client", version.ref = "mqttClient" } -mqtt-buffer = { module = "com.ditchoom:buffer", version.ref = "mqttBuffer" } -mqtt-buffer-codec = { module = "com.ditchoom:buffer-codec", version.ref = "mqttBuffer" } multiplatform-markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "multiplatformMarkdownRenderer" } multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRenderer" } murmurhash = { module = "com.goncalossilva:murmurhash", version.ref = "murmurhash" } diff --git a/jam-server/.gitignore b/jam-server/.gitignore new file mode 100644 index 00000000..a933f108 --- /dev/null +++ b/jam-server/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.wrangler/ +.dev.vars diff --git a/jam-server/README.md b/jam-server/README.md new file mode 100644 index 00000000..c6f7b5e0 --- /dev/null +++ b/jam-server/README.md @@ -0,0 +1,72 @@ +# Spotube Jam server + +The room server behind Group Jam: a Cloudflare Worker with one +[Durable Object](https://developers.cloudflare.com/durable-objects/) per room code. + +Each room relays a few small JSON frames between the host and its guests. Nobody streams +audio through it (every device fetches its own), so a room costs a few KB per minute. + +Rooms are billed per incoming message (20 count as one request) and per storage write, +so steady playback sends almost nothing: + +- The host sends a playback anchor only when playback changes (play/pause, seek, skip, + speed), when its player drifts from its last anchor, and once a minute as a safety net. + Guests extrapolate anchors with the shared clock in between. +- Anchors are kept in memory, not storage. A room that hibernated wakes for a join, and + the host answers the join with a fresh anchor. Only queue changes and membership + bookkeeping are written. +- Keep-alives (`"ping"` → `"pong"`) are answered by the runtime without waking the room. + Clock pings go out on join and every 10 minutes. + +A four-person room sends roughly 200 messages an hour (about 10 billed requests). + +## What a room does + +- **Membership**: tracks who is connected, who hosts, and who is banned. If the host + drops, it keeps hosting for 30 s (so it can reconnect, e.g. after switching networks), then the longest-connected guest + takes over. +- **Retained state**: keeps the host's latest queue (and, while awake, its playback + anchor) so late joiners start in sync. An empty room keeps them for 10 minutes, then deletes itself. +- **Routing**: guest commands (play/pause, skip, seek, suggestions) go to the host. + Kicks and bans are enforced by the server. +- **Clock**: answers pings with its own time. Every device syncs playback to this one + clock (see `JamClock` in the app). + +The wire protocol is in [`src/protocol.ts`](src/protocol.ts). The app side is +`composeApp/src/commonMain/kotlin/org/spotube/spotube/core/jam/`. + +## Develop + +```sh +npm install +npm run dev # http://127.0.0.1:8787 +npm test # smoke test against the dev server (another terminal) +npm run typecheck +``` + +The app's end-to-end tests run against it too: + +```sh +JAM_TEST_URL=http://127.0.0.1:8787 ./gradlew :composeApp:jvmTest --tests 'org.spotube.spotube.core.jam.*' +``` + +## Deploy + +```sh +CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... npx wrangler deploy +``` + +The token needs the "Edit Cloudflare Workers" permissions. Durable Objects on the +SQLite backend work on the free plan. `.github/workflows/jam-server-deploy.yml` deploys on +pushes to `master` that touch this directory (secrets `CLOUDFLARE_API_TOKEN` and +`CLOUDFLARE_ACCOUNT_ID`; optional variable `JAM_SERVER_URL` runs the smoke test after). + +## Self-host + +Deploy to your own Cloudflare account as above, then enter the Worker's URL in +Spotube → Settings → Jam → Server URL. + +## Limits + +- 32 participants per room, 512 KB per frame. +- 60 room connections per minute per IP (`CONNECT_LIMITER` in `wrangler.jsonc`). diff --git a/jam-server/package-lock.json b/jam-server/package-lock.json new file mode 100644 index 00000000..a37d7265 --- /dev/null +++ b/jam-server/package-lock.json @@ -0,0 +1,1606 @@ +{ + "name": "spotube-jam-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spotube-jam-server", + "license": "AGPL-3.0-or-later", + "devDependencies": { + "typescript": "^5.9.3", + "wrangler": "^4.156.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.2.tgz", + "integrity": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20261023.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261023.1.tgz", + "integrity": "sha512-lsEx0i7nR0PNG/erUFFHmQmiidEhLfDE5uQmAL9421/y4/ycuxDDOSaxu3VGGYFFPK5sduyUeMmjIlX4CwOBFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20261023.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261023.1.tgz", + "integrity": "sha512-5e79fHgabdDD8EgmjOhJ1S+xMsf6DbC7fL7IgTBizMnzOp0P2isDn7pYmUxI9AJZE9FVQ4RUilH/I36YNg+GQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20261023.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261023.1.tgz", + "integrity": "sha512-VHQ6jKFSihjR2+x1buby8XTdBLLJztVnYiB7OLOTYO8aDXB+c1Rd2410PdgFuFYc4AgFEk6PCJLyCLL5L1KBnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20261023.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261023.1.tgz", + "integrity": "sha512-ilpRHzfDN8YFRM5CLRKuFw40x/LJBS89yVdhdQmxehlVDuBqjKpJB++zPFbTgKRRFaNupgnXc8RHktXAIhgCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20261023.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261023.1.tgz", + "integrity": "sha512-279WLVjNsMycAwr0EZNiNEf0l2SY6NUtsN80NwZzbTHzakmiN+WSx4w38muxPm+PAU0oSPPAPXKVD+H6Hya8xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "5.20261023.3-beta", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261023.3-beta.tgz", + "integrity": "sha512-thdXzlktu5GE1o/BlmMAPeJ0Cu6Tu5LpMbRlyfRrpVvMRrhd6p52TMqcITCZZLVzt1UO92VGBkLDhLOpryUAyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.5", + "undici": "7.30.1", + "workerd": "1.20261023.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20261023.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261023.1.tgz", + "integrity": "sha512-NeofJFyaYNpqKpFRJW3ApuonaerPd6/JWlfTtLDYf2A6ygqrDEFnYgEXmc0hhOxqnJsSM+kXJN6VebPojzRrsg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20261023.1", + "@cloudflare/workerd-darwin-arm64": "1.20261023.1", + "@cloudflare/workerd-linux-64": "1.20261023.1", + "@cloudflare/workerd-linux-arm64": "1.20261023.1", + "@cloudflare/workerd-windows-64": "1.20261023.1" + } + }, + "node_modules/wrangler": { + "version": "4.156.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.156.0.tgz", + "integrity": "sha512-kVLAShuLiNgE3jcrmXcymDf/iPKDPrSxACe5ueugW/33HeNB9E9CkKYchudCeBwgmsnl4k+1zt4NO7vqb/fRuQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.2", + "miniflare": "5.20261023.3-beta", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20261023.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20261023.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/jam-server/package.json b/jam-server/package.json new file mode 100644 index 00000000..f9db02e2 --- /dev/null +++ b/jam-server/package.json @@ -0,0 +1,22 @@ +{ + "name": "spotube-jam-server", + "private": true, + "description": "Group Jam room server for Spotube (Cloudflare Worker + Durable Objects)", + "license": "AGPL-3.0-or-later", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "types": "wrangler types", + "typecheck": "wrangler types && tsc --noEmit", + "test": "node test/smoke.mjs" + }, + "devDependencies": { + "typescript": "^5.9.3", + "wrangler": "^4.156.0" + }, + "allowScripts": { + "esbuild@0.28.2": true, + "workerd@1.20261023.1": true + } +} diff --git a/jam-server/src/index.ts b/jam-server/src/index.ts new file mode 100644 index 00000000..5a9e27fa --- /dev/null +++ b/jam-server/src/index.ts @@ -0,0 +1,44 @@ +import { CLIENT_ID, MAX_NAME_LENGTH, PROTOCOL_VERSION, ROOM_CODE } from "./protocol"; + +export { JamRoom } from "./room"; + +const JSON_HEADERS = { "content-type": "application/json" }; + +/** + * Routes: + * - `GET /v1/health` → `{ ok, v, now }` (used by the app's "Test connection") + * - `GET /v1/rooms/{CODE}?id=&name=&mode=create|join` (WebSocket) → the room's Durable Object + */ +export default { + async fetch(request, env): Promise { + const url = new URL(request.url); + + if (url.pathname === "/v1/health") { + return Response.json({ ok: true, v: PROTOCOL_VERSION, now: Date.now() }); + } + + const match = url.pathname.match(/^\/v1\/rooms\/([^/]+)$/); + if (!match) return new Response("Not found", { status: 404 }); + + if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") { + return new Response("Expected a WebSocket upgrade", { status: 426 }); + } + + const code = match[1].toUpperCase(); + const id = url.searchParams.get("id") ?? ""; + const name = (url.searchParams.get("name") ?? "").trim().slice(0, MAX_NAME_LENGTH); + const mode = url.searchParams.get("mode"); + if (!ROOM_CODE.test(code) || !CLIENT_ID.test(id) || !name || (mode !== "create" && mode !== "join")) { + return new Response(JSON.stringify({ error: "bad_request" }), { status: 400, headers: JSON_HEADERS }); + } + + const ip = request.headers.get("CF-Connecting-IP") ?? "unknown"; + const { success } = await env.CONNECT_LIMITER.limit({ key: ip }); + if (!success) return new Response("Too many connections", { status: 429 }); + + const forwarded = new URL(request.url); + forwarded.search = new URLSearchParams({ id, name, mode }).toString(); + const room = env.JAM_ROOM.get(env.JAM_ROOM.idFromName(code)); + return room.fetch(new Request(forwarded, request)); + }, +} satisfies ExportedHandler; diff --git a/jam-server/src/protocol.ts b/jam-server/src/protocol.ts new file mode 100644 index 00000000..4aaefb87 --- /dev/null +++ b/jam-server/src/protocol.ts @@ -0,0 +1,85 @@ +/** + * Wire protocol between Spotube and the jam room server (version 1). + * + * Every frame is a JSON object with a `t` discriminator. The server never looks + * inside `body`: queue, anchor and command payloads are owned by the app and only + * routed (and, for queue/anchor, retained) here. + */ + +export const PROTOCOL_VERSION = 1; + +/** Same alphabet as the app's `JamRoomCode` (no I, O, 0, 1). */ +export const ROOM_CODE = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/; +export const CLIENT_ID = /^[A-Za-z0-9_-]{4,64}$/; +export const MAX_NAME_LENGTH = 40; +export const MAX_PARTICIPANTS = 32; +/** A long queue with cover URLs is the largest frame, a few hundred KB at most. */ +export const MAX_FRAME_BYTES = 512 * 1024; + +/** + * A host that drops keeps the room for this long before someone else takes over (long + * enough for a phone to switch networks). Guests keep playing in the meantime. + */ +export const HOST_GRACE_MS = 30_000; +/** An empty room keeps its queue for this long, so everyone can rejoin after a blip. */ +export const EMPTY_ROOM_TTL_MS = 10 * 60_000; + +/** + * Keep-alive, sent as a bare text frame. The runtime answers it without waking the room, + * so it costs nothing; clock pings (`ping` frames) are only sent when the clock syncs. + */ +export const KEEPALIVE_PING = "ping"; +export const KEEPALIVE_PONG = "pong"; + +export type JoinMode = "create" | "join"; + +export type ClientFrame = + | { t: "ping"; id: number; c: number } + | { t: "queue"; body: unknown } + | { t: "anchor"; body: unknown } + | { t: "cmd"; body: unknown } + | { t: "kick"; target: string; ban?: boolean }; + +export interface Participant { + id: string; + name: string; +} + +export type ServerFrame = + | { + t: "welcome"; + v: number; + you: string; + host: string | null; + participants: Participant[]; + queue: unknown; + anchor: unknown; + } + | { t: "pong"; id: number; c: number; s: number } + | { t: "presence"; host: string | null; participants: Participant[] } + | { t: "queue"; body: unknown } + | { t: "anchor"; body: unknown } + | { t: "cmd"; from: string; body: unknown } + | { t: "error"; code: ErrorCode }; + +export type ErrorCode = + | "bad_request" + | "room_taken" + | "room_not_found" + | "room_full" + | "banned" + | "kicked" + | "replaced" + | "frame_too_large"; + +/** WebSocket close codes the app maps back to an [ErrorCode]. */ +export const CLOSE_CODES: Record = { + bad_request: 4000, + room_taken: 4001, + room_not_found: 4004, + room_full: 4005, + banned: 4003, + kicked: 4008, + replaced: 4009, + frame_too_large: 4013, +}; diff --git a/jam-server/src/room.ts b/jam-server/src/room.ts new file mode 100644 index 00000000..72496412 --- /dev/null +++ b/jam-server/src/room.ts @@ -0,0 +1,311 @@ +import { DurableObject } from "cloudflare:workers"; +import { + CLOSE_CODES, + KEEPALIVE_PING, + KEEPALIVE_PONG, + EMPTY_ROOM_TTL_MS, + HOST_GRACE_MS, + MAX_FRAME_BYTES, + MAX_PARTICIPANTS, + PROTOCOL_VERSION, + type ClientFrame, + type ErrorCode, + type JoinMode, + type Participant, + type ServerFrame, +} from "./protocol"; + +/** Kept on each socket so it survives hibernation. */ +interface Attachment { + id: string; + name: string; + joined: number; +} + +const enum Key { + Host = "host", + Queue = "queue", + Banned = "banned", + HostGoneAt = "hostGoneAt", + EmptySince = "emptySince", +} + +/** + * One jam room, named by its six-letter code. + * + * The room is a relay with memory: it tracks who is connected and who hosts, keeps + * the host's latest queue and playback anchor for late joiners, routes guest + * commands to the host, and serves the clock everyone syncs playback against. + * Sockets use the hibernation API, so an idle room costs nothing between messages. + * + * Cost: rooms are billed per incoming message and per storage write, so the steady + * state is kept free of both. Keep-alives are answered by the runtime without waking + * the room, and the anchor (sent on every playback change) lives in memory only: when + * a hibernated room wakes for a join, the host answers the join's presence frame with + * a fresh anchor anyway. + */ +export class JamRoom extends DurableObject { + private anchor: unknown = null; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(KEEPALIVE_PING, KEEPALIVE_PONG)); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const id = url.searchParams.get("id")!; + const name = url.searchParams.get("name")!; + const mode = url.searchParams.get("mode") as JoinMode; + + const { 0: client, 1: server } = new WebSocketPair(); + const previous = this.ctx.getWebSockets(id); + this.ctx.acceptWebSocket(server, [id]); + server.serializeAttachment({ id, name, joined: Date.now() } satisfies Attachment); + + // A reconnect with the same id (network change, app restart) takes over the old socket. + for (const ws of previous) this.reject(ws, "replaced"); + + const refusal = await this.admit(id, mode); + if (refusal) { + this.reject(server, refusal); + } else { + await this.ctx.storage.delete(Key.EmptySince); + const [host, queue] = await Promise.all([ + this.ctx.storage.get(Key.Host), + this.ctx.storage.get(Key.Queue), + ]); + this.send(server, { + t: "welcome", + v: PROTOCOL_VERSION, + you: id, + host: host ?? null, + participants: this.participants(), + queue: queue ?? null, + anchor: this.anchor, + }); + this.broadcast(await this.presence(), server); + } + + return new Response(null, { status: 101, webSocket: client }); + } + + async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise { + const size = typeof message === "string" ? message.length : message.byteLength; + if (size > MAX_FRAME_BYTES) return this.reject(ws, "frame_too_large"); + if (typeof message !== "string") return; + + let frame: ClientFrame; + try { + frame = JSON.parse(message); + } catch { + return; + } + const sender = this.attachment(ws); + + switch (frame.t) { + case "ping": + this.send(ws, { t: "pong", id: frame.id, c: frame.c, s: Date.now() }); + return; + + case "queue": + if (!(await this.isHost(sender.id))) return; + await this.ctx.storage.put(Key.Queue, frame.body); + this.broadcast({ t: "queue", body: frame.body }, ws); + return; + + case "anchor": + if (!(await this.isHost(sender.id))) return; + this.anchor = frame.body; + this.broadcast({ t: "anchor", body: frame.body }, ws); + return; + + case "cmd": { + const host = await this.ctx.storage.get(Key.Host); + const out: ServerFrame = { t: "cmd", from: sender.id, body: frame.body }; + if (sender.id === host) { + this.broadcast(out, ws); + } else if (host) { + for (const target of this.ctx.getWebSockets(host)) this.send(target, out); + } + return; + } + + case "kick": { + if (!(await this.isHost(sender.id)) || frame.target === sender.id) return; + if (frame.ban) { + const banned = (await this.ctx.storage.get(Key.Banned)) ?? []; + if (!banned.includes(frame.target)) { + await this.ctx.storage.put(Key.Banned, [...banned, frame.target]); + } + } + for (const target of this.ctx.getWebSockets(frame.target)) { + this.reject(target, frame.ban ? "banned" : "kicked"); + } + return; + } + } + } + + async webSocketClose(ws: WebSocket, code: number): Promise { + try { + ws.close(code === 1005 ? 1000 : code); + } catch { + // Already closed. + } + await this.onDeparture(ws); + } + + async webSocketError(ws: WebSocket): Promise { + await this.onDeparture(ws); + } + + /** Fires after a host's grace period, or when an empty room expires. */ + async alarm(): Promise { + const live = this.live(); + if (live.length === 0) { + const emptySince = await this.ctx.storage.get(Key.EmptySince); + if (emptySince === undefined) return; + const expiresAt = emptySince + EMPTY_ROOM_TTL_MS; + if (Date.now() >= expiresAt) { + await this.ctx.storage.deleteAll(); + this.anchor = null; + } else { + await this.ctx.storage.setAlarm(expiresAt); + } + return; + } + + const host = await this.ctx.storage.get(Key.Host); + if (host && live.some((ws) => this.attachment(ws).id === host)) return; + await this.promote(); + } + + // ---------- Membership ---------- + + private async admit(id: string, mode: JoinMode): Promise { + const others = this.live().filter((ws) => this.attachment(ws).id !== id); + + if (mode === "create") { + if (others.length > 0) return "room_taken"; + // Leftovers of an expired-but-not-yet-cleaned room must not leak into a new one. + await this.ctx.storage.deleteAll(); + this.anchor = null; + await this.ctx.storage.put(Key.Host, id); + return null; + } + + const [host, banned, queue] = await Promise.all([ + this.ctx.storage.get(Key.Host), + this.ctx.storage.get(Key.Banned), + this.ctx.storage.get(Key.Queue), + ]); + if (host === undefined && queue === undefined && others.length === 0) return "room_not_found"; + if (banned?.includes(id)) return "banned"; + if (others.length >= MAX_PARTICIPANTS) return "room_full"; + + // Joining a room whose host is gone (and whose grace period is over) promotes someone. + const hostLive = host === id || others.some((ws) => this.attachment(ws).id === host); + if (!hostLive) { + const goneAt = await this.ctx.storage.get(Key.HostGoneAt); + if (goneAt === undefined || Date.now() - goneAt >= HOST_GRACE_MS) { + await this.promote(id); + } + } else if (host === id) { + await this.ctx.storage.delete(Key.HostGoneAt); + } + return null; + } + + private async onDeparture(ws: WebSocket): Promise { + const { id } = this.attachment(ws); + const live = this.live().filter((other) => other !== ws); + // Replaced by a newer socket of the same participant: nothing changed. + if (live.some((other) => this.attachment(other).id === id)) return; + + if (live.length === 0) { + await this.ctx.storage.put(Key.EmptySince, Date.now()); + await this.ctx.storage.setAlarm(Date.now() + EMPTY_ROOM_TTL_MS); + return; + } + + if (id === (await this.ctx.storage.get(Key.Host))) { + await this.ctx.storage.put(Key.HostGoneAt, Date.now()); + await this.ctx.storage.setAlarm(Date.now() + HOST_GRACE_MS); + } + this.broadcast(await this.presence(), ws); + } + + /** The longest-connected participant hosts next (or [fallback] in an empty room). */ + private async promote(fallback?: string): Promise { + const next = this.participants()[0]?.id ?? fallback; + if (!next) return; + await this.ctx.storage.put(Key.Host, next); + await this.ctx.storage.delete(Key.HostGoneAt); + this.broadcast(await this.presence()); + } + + private async isHost(id: string): Promise { + return (await this.ctx.storage.get(Key.Host)) === id; + } + + private async presence(): Promise { + return { + t: "presence", + host: (await this.ctx.storage.get(Key.Host)) ?? null, + participants: this.participants(), + }; + } + + // ---------- Sockets ---------- + + private live(): WebSocket[] { + return this.ctx.getWebSockets().filter((ws) => ws.readyState === WebSocket.OPEN); + } + + /** Connected participants, longest-connected first, one entry per id. */ + private participants(): Participant[] { + const byId = new Map(); + for (const ws of this.live()) { + const a = this.attachment(ws); + const existing = byId.get(a.id); + if (!existing || a.joined < existing.joined) byId.set(a.id, a); + } + return [...byId.values()] + .sort((a, b) => a.joined - b.joined || a.id.localeCompare(b.id)) + .map(({ id, name }) => ({ id, name })); + } + + private attachment(ws: WebSocket): Attachment { + return ws.deserializeAttachment() as Attachment; + } + + private send(ws: WebSocket, frame: ServerFrame): void { + try { + ws.send(JSON.stringify(frame)); + } catch { + // The socket closed under us; its close handler cleans up. + } + } + + private broadcast(frame: ServerFrame, except?: WebSocket): void { + const text = JSON.stringify(frame); + for (const ws of this.live()) { + if (ws === except) continue; + try { + ws.send(text); + } catch { + // See send(). + } + } + } + + private reject(ws: WebSocket, code: ErrorCode): void { + this.send(ws, { t: "error", code }); + try { + ws.close(CLOSE_CODES[code], code); + } catch { + // Already closed. + } + } +} diff --git a/jam-server/test/smoke.mjs b/jam-server/test/smoke.mjs new file mode 100644 index 00000000..a2d080dd --- /dev/null +++ b/jam-server/test/smoke.mjs @@ -0,0 +1,131 @@ +// End-to-end smoke test against a running server: `npm run dev`, then `npm test`. +// Point it elsewhere with JAM_URL=https://spotube-jam..workers.dev npm test +import assert from "node:assert/strict"; + +const BASE = process.env.JAM_URL ?? "http://127.0.0.1:8787"; +const WS_BASE = BASE.replace(/^http/, "ws"); +const ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; +const code = () => Array.from({ length: 6 }, () => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join(""); + +function connect(room, id, mode, name = id) { + const ws = new WebSocket(`${WS_BASE}/v1/rooms/${room}?id=${id}&name=${encodeURIComponent(name)}&mode=${mode}`); + const inbox = []; + const waiters = []; + ws.addEventListener("message", (e) => { + const frame = e.data === "pong" ? { t: "keepalive" } : JSON.parse(e.data); + const i = waiters.findIndex((w) => w.match(frame)); + if (i >= 0) waiters.splice(i, 1)[0].resolve(frame); + else inbox.push(frame); + }); + const closed = new Promise((resolve) => ws.addEventListener("close", (e) => resolve(e.code))); + return { + ws, + closed, + send: (frame) => ws.send(JSON.stringify(frame)), + next(t, pred = () => true, timeoutMs = 5_000) { + const match = (f) => f.t === t && pred(f); + const i = inbox.findIndex(match); + if (i >= 0) return Promise.resolve(inbox.splice(i, 1)[0]); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`${id}: timed out waiting for ${t}`)), timeoutMs); + waiters.push({ match, resolve: (f) => (clearTimeout(timer), resolve(f)) }); + }); + }, + }; +} + +const step = async (name, fn) => { + await fn(); + console.log(`ok - ${name}`); +}; + +await step("health", async () => { + const res = await (await fetch(`${BASE}/v1/health`)).json(); + assert.equal(res.ok, true); + assert.equal(res.v, 1); +}); + +const room = code(); +const host = connect(room, "host-0001", "create", "Host"); +let guest; + +await step("host creates the room", async () => { + const welcome = await host.next("welcome"); + assert.equal(welcome.host, "host-0001"); + host.send({ t: "queue", body: { items: [1, 2, 3] } }); +}); + +await step("guest joins and gets the retained queue", async () => { + // The host's queue frame is processed before the guest's join is. + host.send({ t: "ping", id: 1, c: 0 }); + await host.next("pong"); + guest = connect(room, "guest-0001", "join", "Guest"); + const welcome = await guest.next("welcome"); + assert.equal(welcome.host, "host-0001"); + assert.deepEqual(welcome.queue, { items: [1, 2, 3] }); + assert.ok(welcome.participants.some((p) => p.id === "guest-0001")); + await host.next("presence", (f) => f.participants.length === 2); +}); + +await step("clock ping", async () => { + guest.send({ t: "ping", id: 7, c: Date.now() }); + const pong = await guest.next("pong"); + assert.equal(pong.id, 7); + assert.equal(typeof pong.s, "number"); +}); + +await step("keep-alives are answered", async () => { + guest.ws.send("ping"); + await guest.next("keepalive"); +}); + +await step("anchor fans out to guests", async () => { + host.send({ t: "anchor", body: { positionMs: 1234 } }); + const anchor = await guest.next("anchor"); + assert.equal(anchor.body.positionMs, 1234); +}); + +await step("guest anchors are ignored", async () => { + guest.send({ t: "anchor", body: { positionMs: 1 } }); + guest.send({ t: "ping", id: 8, c: 0 }); + await guest.next("pong", (f) => f.id === 8); + await assert.rejects(host.next("anchor", () => true, 500)); +}); + +await step("guest commands reach the host", async () => { + guest.send({ t: "cmd", body: { type: "skipNext" } }); + const cmd = await host.next("cmd"); + assert.equal(cmd.from, "guest-0001"); +}); + +await step("a taken code can't be created again", async () => { + const intruder = connect(room, "intruder-1", "create"); + const error = await intruder.next("error"); + assert.equal(error.code, "room_taken"); + assert.equal(await intruder.closed, 4001); +}); + +await step("unknown rooms are not found", async () => { + const lost = connect(code(), "lost-0001", "join"); + assert.equal((await lost.next("error")).code, "room_not_found"); +}); + +await step("ban closes the socket and blocks rejoining", async () => { + host.send({ t: "kick", target: "guest-0001", ban: true }); + assert.equal((await guest.next("error")).code, "banned"); + assert.equal(await guest.closed, 4003); + const again = connect(room, "guest-0001", "join"); + assert.equal((await again.next("error")).code, "banned"); +}); + +await step("host hands over after the grace period", async () => { + const heir = connect(room, "heir-0001", "join", "Heir"); + await heir.next("welcome"); + host.ws.close(); + const presence = await heir.next("presence", (f) => f.host === "heir-0001", 45_000); + assert.deepEqual(presence.participants.map((p) => p.id), ["heir-0001"]); + heir.ws.close(); +}); + +console.log("all smoke tests passed"); +process.exit(0); diff --git a/jam-server/tsconfig.json b/jam-server/tsconfig.json new file mode 100644 index 00000000..894ca0ef --- /dev/null +++ b/jam-server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "lib": ["es2022"], + "types": ["./worker-configuration.d.ts"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/jam-server/worker-configuration.d.ts b/jam-server/worker-configuration.d.ts new file mode 100644 index 00000000..2d349cde --- /dev/null +++ b/jam-server/worker-configuration.d.ts @@ -0,0 +1,16269 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types` (hash: 28b567dee88147138835f9d78416e7d9) +// Runtime types generated with workerd@1.20261023.1 2026-10-01 +interface __BaseEnv_Env { + CONNECT_LIMITER: RateLimit; + JAM_ROOM: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + durableNamespaces: "JamRoom"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} +declare module "*.txt" { + const value: string; + export default value; +} +declare module "*.html" { + const value: string; + export default value; +} +declare module "*.sql" { + const value: string; + export default value; +} +declare module "*.bin" { + const value: ArrayBuffer; + export default value; +} +declare module "*.wasm" { + const value: WebAssembly.Module; + export default value; +} +declare module "*.wasm?module" { + const value: WebAssembly.Module; + export default value; +} +// Begin runtime types +/*! ***************************************************************************** +Copyright (c) Cloudflare. All rights reserved. +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* eslint-disable */ +// noinspection JSUnusedGlobalSymbols +declare var onmessage: never; +/** + * The **`DOMException`** interface represents an abnormal event (called an exception) that occurs as a result of calling a method or accessing a property of a web API. This is how error conditions are described in web APIs. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) + */ +declare class DOMException extends Error { + constructor(message?: string, name?: string); + /** + * The **`message`** read-only property of the DOMException interface returns a string representing a message or description associated with the given error name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) + */ + readonly message: string; + /** + * The **`name`** read-only property of the DOMException interface returns a string that contains one of the strings associated with an error name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) + */ + readonly name: string; + /** + * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or 0 if none match. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) + */ + readonly code: number; + static readonly INDEX_SIZE_ERR: number; + static readonly DOMSTRING_SIZE_ERR: number; + static readonly HIERARCHY_REQUEST_ERR: number; + static readonly WRONG_DOCUMENT_ERR: number; + static readonly INVALID_CHARACTER_ERR: number; + static readonly NO_DATA_ALLOWED_ERR: number; + static readonly NO_MODIFICATION_ALLOWED_ERR: number; + static readonly NOT_FOUND_ERR: number; + static readonly NOT_SUPPORTED_ERR: number; + static readonly INUSE_ATTRIBUTE_ERR: number; + static readonly INVALID_STATE_ERR: number; + static readonly SYNTAX_ERR: number; + static readonly INVALID_MODIFICATION_ERR: number; + static readonly NAMESPACE_ERR: number; + static readonly INVALID_ACCESS_ERR: number; + static readonly VALIDATION_ERR: number; + static readonly TYPE_MISMATCH_ERR: number; + static readonly SECURITY_ERR: number; + static readonly NETWORK_ERR: number; + static readonly ABORT_ERR: number; + static readonly URL_MISMATCH_ERR: number; + static readonly QUOTA_EXCEEDED_ERR: number; + static readonly TIMEOUT_ERR: number; + static readonly INVALID_NODE_TYPE_ERR: number; + static readonly DATA_CLONE_ERR: number; + get stack(): any; + set stack(value: any); +} +type WorkerGlobalScopeEventMap = { + fetch: FetchEvent; + scheduled: ScheduledEvent; + queue: QueueEvent; + unhandledrejection: PromiseRejectionEvent; + rejectionhandled: PromiseRejectionEvent; +}; +declare abstract class WorkerGlobalScope extends EventTarget { + EventTarget: typeof EventTarget; +} +/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). * + * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) + */ +interface Console { + "assert"(condition?: boolean, ...data: any[]): void; + /** + * The **`console.clear()`** static method clears the console if possible. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) + */ + clear(): void; + /** + * The **`console.count()`** static method logs the number of times that this particular call to count() has been called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) + */ + count(label?: string): void; + /** + * The **`console.countReset()`** static method resets counter used with console.count(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) + */ + countReset(label?: string): void; + /** + * The **`console.debug()`** static method outputs a message to the console at the "debug" log level. The message is only displayed to the user if the console is configured to display debug output. In most cases, the log level is configured within the console UI. This log level might correspond to the Debug or Verbose log level. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) + */ + debug(...data: any[]): void; + /** + * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object. In browser consoles, the output is presented as a hierarchical listing with disclosure triangles that let you see the contents of child objects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) + */ + dir(item?: any, options?: any): void; + /** + * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element. If it is not possible to display as an element the JavaScript Object view is shown instead. The output is presented as a hierarchical listing of expandable nodes that let you see the contents of child nodes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) + */ + dirxml(...data: any[]): void; + /** + * The **`console.error()`** static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) + */ + error(...data: any[]): void; + /** + * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console.groupEnd() is called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) + */ + group(...data: any[]): void; + /** + * The **`console.groupCollapsed()`** static method creates a new inline group in the console. Unlike console.group(), however, the new group is created collapsed. The user will need to use the disclosure button next to it to expand it, revealing the entries created in the group. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) + */ + groupCollapsed(...data: any[]): void; + /** + * The **`console.groupEnd()`** static method exits the current inline group in the console. See Using groups in the console in the console documentation for details and examples. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) + */ + groupEnd(): void; + /** + * The **`console.info()`** static method outputs a message to the console at the "info" log level. The message is only displayed to the user if the console is configured to display info output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as a small "i" icon next to it. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) + */ + info(...data: any[]): void; + /** + * The **`console.log()`** static method outputs a message to the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) + */ + log(...data: any[]): void; + /** + * The **`console.table()`** static method displays tabular data as a table. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) + */ + table(tabularData?: any, properties?: string[]): void; + /** + * The **`console.time()`** static method starts a timer you can use to track how long an operation takes. You give each timer a unique name, and may have up to 10,000 timers running on a given page. When you call console.timeEnd() with the same name, the browser will output the time, in milliseconds, that elapsed since the timer was started. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) + */ + time(label?: string): void; + /** + * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console.time(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) + */ + timeEnd(label?: string): void; + /** + * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console.time(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) + */ + timeLog(label?: string, ...data: any[]): void; + /* The **`console.timeStamp()`** static method adds a single marker to the browser's Performance tool (Firefox bug 1387528, Chrome). This lets you correlate a point in your code with the other events recorded in the timeline, such as layout and paint events. */ + timeStamp(label?: string): void; + /** + * The **`console.trace()`** static method outputs a stack trace to the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) + */ + trace(...data: any[]): void; + /** + * The **`console.warn()`** static method outputs a warning message to the console at the "warning" log level. The message is only displayed to the user if the console is configured to display warning output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as yellow colors and a warning icon. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) + */ + warn(...data: any[]): void; +} +declare const console: Console; +type BufferSource = ArrayBufferView | ArrayBuffer; +type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array; +declare namespace WebAssembly { + class CompileError extends Error { + constructor(message?: string); + } + class RuntimeError extends Error { + constructor(message?: string); + } + type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128"; + interface GlobalDescriptor { + value: ValueType; + mutable?: boolean; + } + class Global { + constructor(descriptor: GlobalDescriptor, value?: any); + value: any; + valueOf(): any; + } + type ImportValue = ExportValue | number; + type ModuleImports = Record; + type Imports = Record; + type ExportValue = Function | Global | Memory | Table; + type Exports = Record; + class Instance { + constructor(module: Module, imports?: Imports); + readonly exports: Exports; + } + interface MemoryDescriptor { + initial: number; + maximum?: number; + shared?: boolean; + } + class Memory { + constructor(descriptor: MemoryDescriptor); + readonly buffer: ArrayBuffer; + grow(delta: number): number; + } + type ImportExportKind = "function" | "global" | "memory" | "table"; + interface ModuleExportDescriptor { + kind: ImportExportKind; + name: string; + } + interface ModuleImportDescriptor { + kind: ImportExportKind; + module: string; + name: string; + } + abstract class Module { + static customSections(module: Module, sectionName: string): ArrayBuffer[]; + static exports(module: Module): ModuleExportDescriptor[]; + static imports(module: Module): ModuleImportDescriptor[]; + } + type TableKind = "anyfunc" | "externref"; + interface TableDescriptor { + element: TableKind; + initial: number; + maximum?: number; + } + class Table { + constructor(descriptor: TableDescriptor, value?: any); + readonly length: number; + get(index: number): any; + grow(delta: number, value?: any): number; + set(index: number, value?: any): void; + } + function instantiate(module: Module, imports?: Imports): Promise; + function validate(bytes: BufferSource): boolean; +} +/** + * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope) + */ +interface ServiceWorkerGlobalScope extends WorkerGlobalScope { + DOMException: typeof DOMException; + WorkerGlobalScope: typeof WorkerGlobalScope; + btoa(data: string): string; + atob(data: string): string; + setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; + setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; + clearTimeout(timeoutId: number | null): void; + setInterval(callback: (...args: any[]) => void, msDelay?: number): number; + setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; + clearInterval(timeoutId: number | null): void; + queueMicrotask(task: Function): void; + structuredClone(value: T, options?: StructuredSerializeOptions): T; + reportError(error: any): void; + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + self: ServiceWorkerGlobalScope; + crypto: Crypto; + caches: CacheStorage; + scheduler: Scheduler; + performance: Performance; + Cloudflare: Cloudflare; + readonly origin: string; + Event: typeof Event; + ExtendableEvent: typeof ExtendableEvent; + CustomEvent: typeof CustomEvent; + PromiseRejectionEvent: typeof PromiseRejectionEvent; + FetchEvent: typeof FetchEvent; + TailEvent: typeof TailEvent; + TraceEvent: typeof TailEvent; + ScheduledEvent: typeof ScheduledEvent; + MessageEvent: typeof MessageEvent; + CloseEvent: typeof CloseEvent; + ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader; + ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader; + ReadableStream: typeof ReadableStream; + WritableStream: typeof WritableStream; + WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter; + TransformStream: typeof TransformStream; + ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy; + CountQueuingStrategy: typeof CountQueuingStrategy; + ErrorEvent: typeof ErrorEvent; + MessageChannel: typeof MessageChannel; + MessagePort: typeof MessagePort; + EventSource: typeof EventSource; + ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest; + ReadableStreamDefaultController: typeof ReadableStreamDefaultController; + ReadableByteStreamController: typeof ReadableByteStreamController; + WritableStreamDefaultController: typeof WritableStreamDefaultController; + TransformStreamDefaultController: typeof TransformStreamDefaultController; + CompressionStream: typeof CompressionStream; + DecompressionStream: typeof DecompressionStream; + TextEncoderStream: typeof TextEncoderStream; + TextDecoderStream: typeof TextDecoderStream; + Headers: typeof Headers; + Body: typeof Body; + Request: typeof Request; + Response: typeof Response; + WebSocket: typeof WebSocket; + WebSocketPair: typeof WebSocketPair; + WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair; + AbortController: typeof AbortController; + AbortSignal: typeof AbortSignal; + TextDecoder: typeof TextDecoder; + TextEncoder: typeof TextEncoder; + navigator: Navigator; + Navigator: typeof Navigator; + URL: typeof URL; + URLSearchParams: typeof URLSearchParams; + URLPattern: typeof URLPattern; + Blob: typeof Blob; + File: typeof File; + FormData: typeof FormData; + Crypto: typeof Crypto; + SubtleCrypto: typeof SubtleCrypto; + CryptoKey: typeof CryptoKey; + CacheStorage: typeof CacheStorage; + Cache: typeof Cache; + FixedLengthStream: typeof FixedLengthStream; + IdentityTransformStream: typeof IdentityTransformStream; + HTMLRewriter: typeof HTMLRewriter; +} +declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void; +declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void; +/** + * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ +declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */ +declare function clearTimeout(timeoutId: number | null): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */ +declare function clearInterval(timeoutId: number | null): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */ +declare function queueMicrotask(task: Function): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */ +declare function structuredClone(value: T, options?: StructuredSerializeOptions): T; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ +declare function reportError(error: any): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ +declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise; +declare const self: ServiceWorkerGlobalScope; +/** +* The Web Crypto API provides a set of low-level functions for common cryptographic tasks. +* The Workers runtime implements the full surface of this API, but with some differences in +* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) +* compared to those implemented in most browsers. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) +*/ +declare const crypto: Crypto; +/** +* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) +*/ +declare const caches: CacheStorage; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/scheduler) */ +declare const scheduler: Scheduler; +/** +* The Workers runtime supports a subset of the Performance API, used to measure timing and performance, +* as well as timing of subrequests and other operations. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) +*/ +declare const performance: Performance; +declare const Cloudflare: Cloudflare; +declare const origin: string; +declare const navigator: Navigator; +interface TestController { +} +interface ExecutionContext { + waitUntil(promise: Promise): void; + passThroughOnException(): void; + readonly exports: Cloudflare.Exports; + readonly props: Props; + cache?: CacheContext; + readonly access?: CloudflareAccessContext; + tracing: Tracing; + abort(reason?: any): void; +} +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | undefined | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + connect?: ExportedHandlerConnectHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; +} +interface StructuredSerializeOptions { + transfer?: any[]; +} +declare abstract class Navigator { + sendBeacon(url: string, body?: BodyInit): boolean; + readonly userAgent: string; + readonly hardwareConcurrency: number; + readonly platform: string; + readonly language: string; + readonly languages: string[]; +} +interface AlarmInvocationInfo { + readonly isRetry: boolean; + readonly retryCount: number; + readonly scheduledTime: number; +} +interface Cloudflare { + readonly compatibilityFlags: Record; +} +interface CachePurgeError { + code: number; + message: string; +} +interface CachePurgeResult { + success: boolean; + errors: CachePurgeError[]; +} +interface CachePurgeOptions { + tags?: string[]; + pathPrefixes?: string[]; + purgeEverything?: boolean; +} +interface CacheContext { + purge(options: CachePurgeOptions): Promise; + invalidate(options: CachePurgeOptions): Promise; +} +interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} +declare abstract class ColoLocalActorNamespace { + get(actorId: string): Fetcher; +} +interface DurableObject { + fetch(request: Request): Response | Promise; + connect?(socket: Socket): void | Promise; + alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise; + webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise; + webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise; + webSocketError?(ws: WebSocket, error: unknown): void | Promise; +} +type DurableObjectStub = Fetcher & { + readonly id: DurableObjectId; + readonly name?: string; +}; +interface DurableObjectId { + toString(): string; + equals(other: DurableObjectId): boolean; + readonly name?: string; + readonly jurisdiction?: string; +} +declare abstract class DurableObjectNamespace { + newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId; + idFromName(name: string): DurableObjectId; + idFromString(id: string): DurableObjectId; + get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub; + getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub; + jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace; +} +type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high" | "us"; +interface DurableObjectNamespaceNewUniqueIdOptions { + jurisdiction?: DurableObjectJurisdiction; +} +type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me"; +type DurableObjectRoutingMode = "primary-only"; +interface DurableObjectNamespaceGetDurableObjectOptions { + locationHint?: DurableObjectLocationHint; + routingMode?: DurableObjectRoutingMode; +} +interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { +} +interface DurableObjectState { + waitUntil(promise: Promise): void; + readonly exports: Cloudflare.Exports; + readonly props: Props; + readonly id: DurableObjectId; + readonly storage: DurableObjectStorage; + container?: Container; + facets: DurableObjectFacets; + blockConcurrencyWhile(callback: () => Promise): Promise; + acceptWebSocket(ws: WebSocket, tags?: string[]): void; + getWebSockets(tag?: string): WebSocket[]; + setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void; + getWebSocketAutoResponse(): WebSocketRequestResponsePair | null; + getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null; + setHibernatableWebSocketEventTimeout(timeoutMs?: number): void; + getHibernatableWebSocketEventTimeout(): number | null; + getTags(ws: WebSocket): string[]; + abort(reason?: string, options?: DurableObjectAbortOptions): void; +} +interface DurableObjectTransaction { + get(key: string, options?: DurableObjectGetOptions): Promise; + get(keys: string[], options?: DurableObjectGetOptions): Promise>; + list(options?: DurableObjectListOptions): Promise>; + put(key: string, value: T, options?: DurableObjectPutOptions): Promise; + put(entries: Record, options?: DurableObjectPutOptions): Promise; + delete(key: string, options?: DurableObjectPutOptions): Promise; + delete(keys: string[], options?: DurableObjectPutOptions): Promise; + rollback(): void; + getAlarm(options?: DurableObjectGetAlarmOptions): Promise; + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise; + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise; +} +interface DurableObjectStorage { + get(key: string, options?: DurableObjectGetOptions): Promise; + get(keys: string[], options?: DurableObjectGetOptions): Promise>; + list(options?: DurableObjectListOptions): Promise>; + put(key: string, value: T, options?: DurableObjectPutOptions): Promise; + put(entries: Record, options?: DurableObjectPutOptions): Promise; + delete(key: string, options?: DurableObjectPutOptions): Promise; + delete(keys: string[], options?: DurableObjectPutOptions): Promise; + deleteAll(options?: DurableObjectPutOptions): Promise; + transaction(closure: (txn: DurableObjectTransaction) => Promise): Promise; + getAlarm(options?: DurableObjectGetAlarmOptions): Promise; + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise; + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise; + sync(): Promise; + sql: SqlStorage; + kv: SyncKvStorage; + transactionSync(closure: () => T): T; + getCurrentBookmark(): Promise; + getBookmarkForTime(timestamp: number | Date): Promise; + onNextSessionRestoreBookmark(bookmark: string): Promise; +} +interface DurableObjectAbortOptions { + retryAlarm?: boolean; +} +interface DurableObjectListOptions { + start?: string; + startAfter?: string; + end?: string; + prefix?: string; + reverse?: boolean; + limit?: number; + allowConcurrency?: boolean; + noCache?: boolean; +} +interface DurableObjectGetOptions { + allowConcurrency?: boolean; + noCache?: boolean; +} +interface DurableObjectGetAlarmOptions { + allowConcurrency?: boolean; +} +interface DurableObjectPutOptions { + allowConcurrency?: boolean; + allowUnconfirmed?: boolean; + noCache?: boolean; +} +interface DurableObjectSetAlarmOptions { + allowConcurrency?: boolean; + allowUnconfirmed?: boolean; +} +declare class WebSocketRequestResponsePair { + constructor(request: string, response: string); + get request(): string; + get response(): string; +} +interface DurableObjectFacets { + get(name: string, getStartupOptions: () => FacetStartupOptions | Promise>): Fetcher; + abort(name: string, reason: any): void; + delete(name: string): void; + clone(src: string, dst: string): void; +} +interface FacetStartupOptions { + id?: DurableObjectId | string; + class: DurableObjectClass; +} +interface AnalyticsEngineDataset { + writeDataPoint(event?: AnalyticsEngineDataPoint): void; +} +interface AnalyticsEngineDataPoint { + indexes?: ((ArrayBuffer | string) | null)[]; + doubles?: number[]; + blobs?: ((ArrayBuffer | string) | null)[]; +} +/** + * The **`Event`** interface represents an event which takes place on an EventTarget. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event) + */ +declare class Event { + constructor(type: string, init?: EventInit); + /** + * The **`type`** read-only property of the Event interface returns a string containing the event's type. It is set when the event is constructed and is the name commonly used to refer to the specific event, such as click, load, or error. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type) + */ + get type(): string; + /** + * The **`eventPhase`** read-only property of the Event interface indicates which phase of the event flow is currently being evaluated. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase) + */ + get eventPhase(): number; + /** + * The read-only **`composed`** property of the Event interface returns a boolean value which indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed) + */ + get composed(): boolean; + /** + * The **`bubbles`** read-only property of the Event interface indicates whether the event bubbles up through the DOM tree or not. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles) + */ + get bubbles(): boolean; + /** + * The **`cancelable`** read-only property of the Event interface indicates whether the event can be canceled, and therefore prevented as if the event never happened. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable) + */ + get cancelable(): boolean; + /** + * The **`defaultPrevented`** read-only property of the Event interface returns a boolean value indicating whether or not the call to Event.preventDefault() canceled the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented) + */ + get defaultPrevented(): boolean; + /** + * The Event property **`returnValue`** indicates whether the default action for this event has been prevented or not. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue) + */ + get returnValue(): boolean; + /** + * The **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget) + */ + get currentTarget(): EventTarget | undefined; + /** + * The read-only **`target`** property of the Event interface is a reference to the object onto which the event was dispatched. It is different from Event.currentTarget when the event handler is called during the bubbling or capturing phase of the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target) + */ + get target(): EventTarget | undefined; + /** + * The deprecated **`Event.srcElement`** is an alias for the Event.target property. Use Event.target instead. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement) + */ + get srcElement(): EventTarget | undefined; + /** + * The **`timeStamp`** read-only property of the Event interface returns the time (in milliseconds) at which the event was created. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp) + */ + get timeStamp(): number; + /** + * The **`isTrusted`** read-only property of the Event interface is a boolean value that is true when the event was generated by the user agent (including via user actions and programmatic methods such as HTMLElement.focus()), and false when the event was dispatched via EventTarget.dispatchEvent(). The only exception is the click event, which initializes the isTrusted property to false in user agents. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted) + */ + get isTrusted(): boolean; + /** + * The **`cancelBubble`** property of the Event interface is deprecated. Use Event.stopPropagation() instead. Setting its value to true before returning from an event handler prevents propagation of the event. In later implementations, setting this to false does nothing. See Browser compatibility for details. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + get cancelBubble(): boolean; + /** + * The **`cancelBubble`** property of the Event interface is deprecated. Use Event.stopPropagation() instead. Setting its value to true before returning from an event handler prevents propagation of the event. In later implementations, setting this to false does nothing. See Browser compatibility for details. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + set cancelBubble(value: boolean); + /** + * The **`stopImmediatePropagation()`** method of the Event interface prevents other listeners of the same event from being called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation) + */ + stopImmediatePropagation(): void; + /** + * The **`preventDefault()`** method of the Event interface tells the user agent that the event is being explicitly handled, so its default action, such as page scrolling, link navigation, or pasting text, should not be taken. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault) + */ + preventDefault(): void; + /** + * The **`stopPropagation()`** method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases. It does not, however, prevent any default behaviors from occurring; for instance, clicks on links are still processed. If you want to stop those behaviors, see the preventDefault() method. It also does not prevent propagation to other event-handlers of the current element. If you want to stop those, see stopImmediatePropagation(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation) + */ + stopPropagation(): void; + /** + * The **`composedPath()`** method of the Event interface returns the event's path which is an array of the objects on which listeners will be invoked. This does not include nodes in shadow trees if the shadow root was created with its ShadowRoot.mode closed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath) + */ + composedPath(): EventTarget[]; + static readonly NONE: number; + static readonly CAPTURING_PHASE: number; + static readonly AT_TARGET: number; + static readonly BUBBLING_PHASE: number; +} +interface EventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; +} +type EventListener = (event: EventType) => void; +interface EventListenerObject { + handleEvent(event: EventType): void; +} +type EventListenerOrEventListenerObject = EventListener | EventListenerObject; +/** + * The **`EventTarget`** interface is implemented by objects that can receive events and may have listeners for them. In other words, any target of events implements the three methods associated with this interface. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget) + */ +declare class EventTarget = Record> { + constructor(); + /** + * The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener) + */ + addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void; + /** + * The **`removeEventListener()`** method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target. The event listener to be removed is identified using a combination of the event type, the event listener function itself, and various optional options that may affect the matching process; see Matching event listeners for removal. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener) + */ + removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void; + /** + * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ + dispatchEvent(event: EventMap[keyof EventMap]): boolean; +} +interface EventTargetEventListenerOptions { + capture?: boolean; +} +interface EventTargetAddEventListenerOptions { + capture?: boolean; + passive?: boolean; + once?: boolean; + signal?: AbortSignal; +} +interface EventTargetHandlerObject { + handleEvent: (event: Event) => any | undefined; +} +/** + * The **`AbortController`** interface represents a controller object that allows you to abort one or more Web requests as and when desired. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController) + */ +declare class AbortController { + constructor(); + /** + * The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal) + */ + get signal(): AbortSignal; + /** + * The **`abort()`** method of the AbortController interface aborts an asynchronous operation before it has completed. This is able to abort fetch requests, the consumption of any response bodies, or streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort) + */ + abort(reason?: any): void; +} +/** + * The **`AbortSignal`** interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal) + */ +declare abstract class AbortSignal extends EventTarget { + /** + * The **`AbortSignal.abort()`** static method returns an AbortSignal that is already set as aborted (and which does not trigger an abort event). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static) + */ + static abort(reason?: any): AbortSignal; + /** + * The **`AbortSignal.timeout()`** static method returns an AbortSignal that will automatically abort after a specified time. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static) + */ + static timeout(delay: number): AbortSignal; + /** + * The **`AbortSignal.any()`** static method takes an iterable of abort signals and returns an AbortSignal. The returned abort signal is aborted when any of the input iterable abort signals are aborted. The abort reason will be set to the reason of the first signal that is aborted. If any of the given abort signals are already aborted then so will be the returned AbortSignal. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static) + */ + static any(signals: AbortSignal[]): AbortSignal; + /** + * The **`aborted`** read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (true) or not (false). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted) + */ + get aborted(): boolean; + /** + * The **`reason`** read-only property returns a JavaScript value that indicates the abort reason. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason) + */ + get reason(): any; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + get onabort(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + set onabort(value: any | null); + /** + * The **`throwIfAborted()`** method throws the signal's abort reason if the signal has been aborted; otherwise it does nothing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) + */ + throwIfAborted(): void; +} +/** + * The **`Scheduler`** interface of the Prioritized Task Scheduling API provides methods for scheduling prioritized tasks. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Scheduler) + */ +interface Scheduler { + wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise; +} +interface SchedulerWaitOptions { + signal?: AbortSignal; +} +/** + * The **`ExtendableEvent`** interface extends the lifetime of the install and activate events dispatched on the global scope as part of the service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent) + */ +declare abstract class ExtendableEvent extends Event { + /** + * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing. It can also be used to detect whether that work was successful. In service workers, waitUntil() tells the browser that work is ongoing until the promise settles, and it shouldn't terminate the service worker if it wants that work to complete. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) + */ + waitUntil(promise: Promise): void; +} +/** + * The **`CustomEvent`** interface can be used to attach custom data to an event generated by an application. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) + */ +declare class CustomEvent extends Event { + constructor(type: string, init?: CustomEventCustomEventInit); + /** + * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail) + */ + get detail(): T; +} +interface CustomEventCustomEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + detail?: any; +} +/** + * The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob) + */ +declare class Blob { + constructor(bits?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions); + /** + * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) + */ + get size(): number; + /** + * The **`type`** read-only property of the Blob interface returns the MIME type of the file. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) + */ + get type(): string; + /** + * The **`slice()`** method of the Blob interface creates and returns a new Blob object which contains data from a subset of the blob on which it's called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) + */ + slice(start?: number, end?: number, type?: string): Blob; + /** + * The **`arrayBuffer()`** method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer) + */ + arrayBuffer(): Promise; + /** + * The **`bytes()`** method of the Blob interface returns a Promise that resolves with a Uint8Array containing the contents of the blob as an array of bytes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes) + */ + bytes(): Promise; + /** + * The **`text()`** method of the Blob interface returns a Promise that resolves with a string containing the contents of the blob, interpreted as UTF-8. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) + */ + text(): Promise; + /** + * The **`stream()`** method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the Blob. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream) + */ + stream(): ReadableStream; +} +interface BlobOptions { + type?: string; +} +/** + * The **`File`** interface provides information about files and allows JavaScript in a web page to access their content. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File) + */ +declare class File extends Blob { + constructor(bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, name: string, options?: FileOptions); + /** + * The **`name`** read-only property of the File interface returns the name of the file represented by a File object. For security reasons, the path is excluded from this property. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) + */ + get name(): string; + /** + * The **`lastModified`** read-only property of the File interface provides the last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) + */ + get lastModified(): number; +} +interface FileOptions { + type?: string; + lastModified?: number; +} +/** +* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) +*/ +declare abstract class CacheStorage { + /** + * The **`open()`** method of the CacheStorage interface returns a Promise that resolves to the Cache object matching the cacheName. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open) + */ + open(cacheName: string): Promise; + readonly default: Cache; +} +/** +* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) +*/ +declare abstract class Cache { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */ + delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */ + match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */ + put(request: RequestInfo | URL, response: Response): Promise; +} +interface CacheQueryOptions { + ignoreMethod?: boolean; +} +/** +* The Web Crypto API provides a set of low-level functions for common cryptographic tasks. +* The Workers runtime implements the full surface of this API, but with some differences in +* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) +* compared to those implemented in most browsers. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) +*/ +declare abstract class Crypto { + /** + * The **`Crypto.subtle`** read-only property returns a SubtleCrypto which can then be used to perform low-level cryptographic operations. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) + */ + get subtle(): SubtleCrypto; + /** + * The **`Crypto.getRandomValues()`** method lets you get cryptographically strong random values. The array given as the parameter is filled with random numbers (random in its cryptographic meaning). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) + */ + getRandomValues(buffer: T): T; + /** + * The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID) + */ + randomUUID(): string; + DigestStream: typeof DigestStream; +} +/** + * The **`SubtleCrypto`** interface of the Web Crypto API provides a number of low-level cryptographic functions. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) + */ +declare abstract class SubtleCrypto { + /** + * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) + */ + encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, plainText: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`decrypt()`** method of the SubtleCrypto interface decrypts some encrypted data. It takes as arguments a key to decrypt with, some optional extra parameters, and the data to decrypt (also known as "ciphertext"). It returns a Promise which will be fulfilled with the decrypted data (also known as "plaintext"). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) + */ + decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, cipherText: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`sign()`** method of the SubtleCrypto interface generates a digital signature. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) + */ + sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`verify()`** method of the SubtleCrypto interface verifies a digital signature. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) + */ + verify(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, signature: ArrayBuffer | ArrayBufferView, data: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`digest()`** method of the SubtleCrypto interface generates a digest of the given data, using the specified hash function. A digest is a short fixed-length value derived from some variable-length input. Cryptographic digests should exhibit collision-resistance, meaning that it's hard to come up with two different inputs that have the same digest value. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) + */ + digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`generateKey()`** method of the SubtleCrypto interface is used to generate a new key (for symmetric algorithms) or key pair (for public-key algorithms). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) + */ + generateKey(algorithm: string | SubtleCryptoGenerateKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + /** + * The **`deriveKey()`** method of the SubtleCrypto interface can be used to derive a secret key from a master key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) + */ + deriveKey(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + /** + * The **`deriveBits()`** method of the SubtleCrypto interface can be used to derive an array of bits from a base key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) + */ + deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, length?: number | null): Promise; + /** + * The **`importKey()`** method of the SubtleCrypto interface imports a key: that is, it takes as input a key in an external, portable format and gives you a CryptoKey object that you can use in the Web Crypto API. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) + */ + importKey(format: string, keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, algorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + /** + * The **`exportKey()`** method of the SubtleCrypto interface exports a key: that is, it takes as input a CryptoKey object and gives you the key in an external, portable format. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) + */ + exportKey(format: string, key: CryptoKey): Promise; + /** + * The **`wrapKey()`** method of the SubtleCrypto interface "wraps" a key. This means that it exports the key in an external, portable format, then encrypts the exported key. Wrapping a key helps protect it in untrusted environments, such as inside an otherwise unprotected data store or in transmission over an unprotected network. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) + */ + wrapKey(format: string, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm): Promise; + /** + * The **`unwrapKey()`** method of the SubtleCrypto interface "unwraps" a key. This means that it takes as its input a key that has been exported and then encrypted (also called "wrapped"). It decrypts the key and then imports it, returning a CryptoKey object that can be used in the Web Crypto API. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) + */ + unwrapKey(format: string, wrappedKey: ArrayBuffer | ArrayBufferView, unwrappingKey: CryptoKey, unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean; +} +/** + * The **`CryptoKey`** interface of the Web Crypto API represents a cryptographic key obtained from one of the SubtleCrypto methods generateKey(), deriveKey(), importKey(), or unwrapKey(). + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey) + */ +declare abstract class CryptoKey { + /** + * The read-only **`type`** property of the CryptoKey interface indicates which kind of key is represented by the object. It can have the following values: + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) + */ + readonly type: string; + /** + * The read-only **`extractable`** property of the CryptoKey interface indicates whether or not the key may be extracted using SubtleCrypto.exportKey() or SubtleCrypto.wrapKey(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) + */ + readonly extractable: boolean; + /** + * The read-only **`algorithm`** property of the CryptoKey interface returns an object describing the algorithm for which this key can be used, and any associated extra parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) + */ + readonly algorithm: CryptoKeyKeyAlgorithm | CryptoKeyAesKeyAlgorithm | CryptoKeyHmacKeyAlgorithm | CryptoKeyRsaKeyAlgorithm | CryptoKeyEllipticKeyAlgorithm | CryptoKeyArbitraryKeyAlgorithm; + /** + * The read-only **`usages`** property of the CryptoKey interface indicates what can be done with the key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) + */ + readonly usages: string[]; +} +interface CryptoKeyPair { + publicKey: CryptoKey; + privateKey: CryptoKey; +} +interface JsonWebKey { + kty: string; + use?: string; + key_ops?: string[]; + alg?: string; + ext?: boolean; + crv?: string; + x?: string; + y?: string; + d?: string; + n?: string; + e?: string; + p?: string; + q?: string; + dp?: string; + dq?: string; + qi?: string; + oth?: RsaOtherPrimesInfo[]; + k?: string; + pub?: string; + priv?: string; +} +interface RsaOtherPrimesInfo { + r?: string; + d?: string; + t?: string; +} +interface SubtleCryptoDeriveKeyAlgorithm { + name: string; + salt?: (ArrayBuffer | ArrayBufferView); + iterations?: number; + hash?: (string | SubtleCryptoHashAlgorithm); + $public?: CryptoKey; + info?: (ArrayBuffer | ArrayBufferView); +} +interface SubtleCryptoEncryptAlgorithm { + name: string; + iv?: (ArrayBuffer | ArrayBufferView); + additionalData?: (ArrayBuffer | ArrayBufferView); + tagLength?: number; + counter?: (ArrayBuffer | ArrayBufferView); + length?: number; + label?: (ArrayBuffer | ArrayBufferView); +} +interface SubtleCryptoGenerateKeyAlgorithm { + name: string; + hash?: (string | SubtleCryptoHashAlgorithm); + modulusLength?: number; + publicExponent?: (ArrayBuffer | ArrayBufferView); + length?: number; + namedCurve?: string; +} +interface SubtleCryptoHashAlgorithm { + name: string; +} +interface SubtleCryptoImportKeyAlgorithm { + name: string; + hash?: (string | SubtleCryptoHashAlgorithm); + length?: number; + namedCurve?: string; + compressed?: boolean; +} +interface SubtleCryptoSignAlgorithm { + name: string; + hash?: (string | SubtleCryptoHashAlgorithm); + dataLength?: number; + saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; +} +interface CryptoKeyKeyAlgorithm { + name: string; +} +interface CryptoKeyAesKeyAlgorithm { + name: string; + length: number; +} +interface CryptoKeyHmacKeyAlgorithm { + name: string; + hash: CryptoKeyKeyAlgorithm; + length: number; +} +interface CryptoKeyRsaKeyAlgorithm { + name: string; + modulusLength: number; + publicExponent: ArrayBuffer | ArrayBufferView; + hash?: CryptoKeyKeyAlgorithm; +} +interface CryptoKeyEllipticKeyAlgorithm { + name: string; + namedCurve: string; +} +interface CryptoKeyArbitraryKeyAlgorithm { + name: string; + hash?: CryptoKeyKeyAlgorithm; + namedCurve?: string; + length?: number; +} +declare class DigestStream extends WritableStream { + constructor(algorithm: string | SubtleCryptoHashAlgorithm, options?: DigestStreamOptions); + readonly digest: Promise; + get bytesWritten(): number | bigint; +} +interface DigestStreamOptions { + toWellFormed?: boolean; +} +/** + * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as UTF-8, ISO-8859-2, or GBK. A decoder takes an array of bytes as input and returns a JavaScript string. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) + */ +declare class TextDecoder { + constructor(label?: string, options?: TextDecoderConstructorOptions); + /** + * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) + */ + decode(input?: (ArrayBuffer | ArrayBufferView), options?: TextDecoderDecodeOptions): string; + get encoding(): string; + get fatal(): boolean; + get ignoreBOM(): boolean; +} +/** + * The **`TextEncoder`** interface enables you to encode a JavaScript string using UTF-8. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) + */ +declare class TextEncoder { + constructor(); + /** + * The **`TextEncoder.encode()`** method takes a string as input, and returns a Uint8Array containing the string encoded using UTF-8. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) + */ + encode(input?: string): Uint8Array; + /** + * The **`TextEncoder.encodeInto()`** method takes a string to encode and a destination Uint8Array to put resulting UTF-8 encoded text into, and returns an object indicating the progress of the encoding. This is potentially more performant than the encode() method — especially when the target buffer is a view into a Wasm heap. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) + */ + encodeInto(input: string, buffer: Uint8Array): TextEncoderEncodeIntoResult; + get encoding(): string; +} +interface TextDecoderConstructorOptions { + fatal: boolean; + ignoreBOM: boolean; +} +interface TextDecoderDecodeOptions { + stream: boolean; +} +interface TextEncoderEncodeIntoResult { + read: number; + written: number; +} +/** + * The **`ErrorEvent`** interface represents events providing information related to errors in scripts or in files. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent) + */ +declare class ErrorEvent extends Event { + constructor(type: string, init?: ErrorEventErrorEventInit); + /** + * The **`filename`** read-only property of the ErrorEvent interface returns a string containing the name of the script file in which the error occurred. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename) + */ + get filename(): string; + /** + * The **`message`** read-only property of the ErrorEvent interface returns a string containing a human-readable error message describing the problem. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message) + */ + get message(): string; + /** + * The **`lineno`** read-only property of the ErrorEvent interface returns an integer containing the line number of the script file on which the error occurred. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno) + */ + get lineno(): number; + /** + * The **`colno`** read-only property of the ErrorEvent interface returns an integer containing the column number of the script file on which the error occurred. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno) + */ + get colno(): number; + /** + * The **`error`** read-only property of the ErrorEvent interface returns a JavaScript value, such as an Error or DOMException, representing the error associated with this event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error) + */ + get error(): any; +} +interface ErrorEventErrorEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + message?: string; + filename?: string; + lineno?: number; + colno?: number; + error?: any; +} +/** + * The **`MessageEvent`** interface represents a message received by a target object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) + */ +declare class MessageEvent extends Event { + constructor(type: string, initializer?: MessageEventInit); + /** + * The **`data`** read-only property of the MessageEvent interface represents the data sent by the message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) + */ + readonly data: any; + /** + * The **`origin`** read-only property of the MessageEvent interface is a string representing the origin of the message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) + */ + readonly origin: string | null; + /** + * The **`lastEventId`** read-only property of the MessageEvent interface is a string representing a unique ID for the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId) + */ + readonly lastEventId: string; + /** + * The **`source`** read-only property of the MessageEvent interface is a MessageEventSource (which can be a WindowProxy, MessagePort, or ServiceWorker object) representing the message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source) + */ + readonly source: MessagePort | null; + /** + * The **`ports`** read-only property of the MessageEvent interface is an array of MessagePort objects containing all MessagePort objects sent with the message, in order. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports) + */ + readonly ports: MessagePort[]; +} +interface MessageEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + data?: any; + origin?: string; + lastEventId?: string; + source?: (MessagePort | null); + ports?: MessagePort[]; +} +/** + * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. These events are particularly useful for telemetry and debugging purposes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent) + */ +declare abstract class PromiseRejectionEvent extends Event { + /** + * The PromiseRejectionEvent interface's **`promise`** read-only property indicates the JavaScript Promise which was rejected. You can examine the event's PromiseRejectionEvent.reason property to learn why the promise was rejected. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise) + */ + readonly promise: Promise; + /** + * The PromiseRejectionEvent **`reason`** read-only property is any JavaScript value or Object which provides the reason passed into Promise.reject(). This in theory provides information about why the promise was rejected. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason) + */ + readonly reason: any; +} +/** + * The **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the fetch(), XMLHttpRequest.send() or navigator.sendBeacon() methods. It uses the same format a form would use if the encoding type were set to "multipart/form-data". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData) + */ +declare class FormData { + constructor(); + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: string | Blob): void; + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: string): void; + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: Blob, filename?: string): void; + /** + * The **`delete()`** method of the FormData interface deletes a key and its value(s) from a FormData object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete) + */ + delete(name: string): void; + /** + * The **`get()`** method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get) + */ + get(name: string): (File | string) | null; + /** + * The **`getAll()`** method of the FormData interface returns all the values associated with a given key from within a FormData object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll) + */ + getAll(name: string): (File | string)[]; + /** + * The **`has()`** method of the FormData interface returns whether a FormData object contains a certain key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) + */ + has(name: string): boolean; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: string | Blob): void; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: string): void; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: Blob, filename?: string): void; + entries(): IterableIterator<[ + key: string, + value: File | string + ]>; + keys(): IterableIterator; + values(): IterableIterator<(File | string)>; + forEach(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void; + [Symbol.iterator](): IterableIterator<[ + key: string, + value: File | string + ]>; +} +interface ContentOptions { + html?: boolean; +} +declare class HTMLRewriter { + constructor(); + on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter; + onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter; + transform(response: Response): Response; +} +interface HTMLRewriterElementContentHandlers { + element?(element: Element): void | Promise; + comments?(comment: Comment): void | Promise; + text?(element: Text): void | Promise; +} +interface HTMLRewriterDocumentContentHandlers { + doctype?(doctype: Doctype): void | Promise; + comments?(comment: Comment): void | Promise; + text?(text: Text): void | Promise; + end?(end: DocumentEnd): void | Promise; +} +interface Doctype { + readonly name: string | null; + readonly publicId: string | null; + readonly systemId: string | null; +} +interface Element { + tagName: string; + readonly attributes: IterableIterator; + readonly removed: boolean; + readonly namespaceURI: string; + getAttribute(name: string): string | null; + hasAttribute(name: string): boolean; + setAttribute(name: string, value: string): Element; + removeAttribute(name: string): Element; + before(content: string | ReadableStream | Response, options?: ContentOptions): Element; + after(content: string | ReadableStream | Response, options?: ContentOptions): Element; + prepend(content: string | ReadableStream | Response, options?: ContentOptions): Element; + append(content: string | ReadableStream | Response, options?: ContentOptions): Element; + replace(content: string | ReadableStream | Response, options?: ContentOptions): Element; + remove(): Element; + removeAndKeepContent(): Element; + setInnerContent(content: string | ReadableStream | Response, options?: ContentOptions): Element; + onEndTag(handler: (tag: EndTag) => void | Promise): void; +} +interface EndTag { + name: string; + before(content: string | ReadableStream | Response, options?: ContentOptions): EndTag; + after(content: string | ReadableStream | Response, options?: ContentOptions): EndTag; + remove(): EndTag; +} +interface Comment { + text: string; + readonly removed: boolean; + before(content: string, options?: ContentOptions): Comment; + after(content: string, options?: ContentOptions): Comment; + replace(content: string, options?: ContentOptions): Comment; + remove(): Comment; +} +interface Text { + readonly text: string; + readonly lastInTextNode: boolean; + readonly removed: boolean; + before(content: string | ReadableStream | Response, options?: ContentOptions): Text; + after(content: string | ReadableStream | Response, options?: ContentOptions): Text; + replace(content: string | ReadableStream | Response, options?: ContentOptions): Text; + remove(): Text; +} +interface DocumentEnd { + append(content: string, options?: ContentOptions): DocumentEnd; +} +/** + * This is the event type for fetch events dispatched on the service worker global scope. It contains information about the fetch, including the request and how the receiver will treat the response. It provides the event.respondWith() method, which allows us to provide a response to this fetch. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent) + */ +declare abstract class FetchEvent extends ExtendableEvent { + /** + * The **`request`** read-only property of the FetchEvent interface returns the Request that triggered the event handler. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request) + */ + readonly request: Request; + /** + * The **`respondWith()`** method of FetchEvent prevents the browser's default fetch handling, and allows you to provide a promise for a Response yourself. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/respondWith) + */ + respondWith(promise: Response | Promise): void; + passThroughOnException(): void; +} +type HeadersInit = Headers | Iterable> | Record; +/** + * The **`Headers`** interface of the Fetch API allows you to perform various actions on HTTP request and response headers. These actions include retrieving, setting, adding to, and removing headers from the list of the request's headers. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers) + */ +declare class Headers { + constructor(init?: HeadersInit); + /** + * The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a Headers object with a given name. If the requested header doesn't exist in the Headers object, it returns null. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) + */ + get(name: string): string | null; + getAll(name: string): string[]; + /** + * The **`getSetCookie()`** method of the Headers interface returns an array containing the values of all Set-Cookie headers associated with a response. This allows Headers objects to handle having multiple Set-Cookie headers, which wasn't possible prior to its implementation. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) + */ + getSetCookie(): string[]; + /** + * The **`has()`** method of the Headers interface returns a boolean stating whether a Headers object contains a certain header. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) + */ + has(name: string): boolean; + /** + * The **`set()`** method of the Headers interface sets a new value for an existing header inside a Headers object, or adds the header if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) + */ + set(name: string, value: string): void; + /** + * The **`append()`** method of the Headers interface appends a new value onto an existing header inside a Headers object, or adds the header if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) + */ + append(name: string, value: string): void; + /** + * The **`delete()`** method of the Headers interface deletes a header from the current Headers object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) + */ + delete(name: string): void; + forEach(callback: (this: This, value: string, key: string, parent: Headers) => void, thisArg?: This): void; + entries(): IterableIterator<[ + key: string, + value: string + ]>; + keys(): IterableIterator; + values(): IterableIterator; + [Symbol.iterator](): IterableIterator<[ + key: string, + value: string + ]>; +} +type BodyInit = ReadableStream | string | ArrayBuffer | ArrayBufferView | Blob | URLSearchParams | FormData | Iterable | AsyncIterable; +declare abstract class Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ + get body(): ReadableStream | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + get bodyUsed(): boolean; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */ + bytes(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */ + text(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */ + json(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */ + formData(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob(): Promise; +} +/** + * The **`Response`** interface of the Fetch API represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +declare var Response: { + prototype: Response; + new (body?: BodyInit | null, init?: ResponseInit): Response; + error(): Response; + redirect(url: string, status?: number): Response; + json(any: any, maybeInit?: (ResponseInit | Response)): Response; +}; +/** + * The **`Response`** interface of the Fetch API represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +interface Response extends Body { + /** + * The **`clone()`** method of the Response interface creates a clone of a response object, identical in every way, but stored in a different variable. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/clone) + */ + clone(): Response; + /** + * The **`status`** read-only property of the Response interface contains the HTTP status codes of the response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status) + */ + status: number; + /** + * The **`statusText`** read-only property of the Response interface contains the status message corresponding to the HTTP status code in Response.status. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText) + */ + statusText: string; + /** + * The **`headers`** read-only property of the Response interface contains the Headers object associated with the response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) + */ + headers: Headers; + /** + * The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok) + */ + ok: boolean; + /** + * The **`redirected`** read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/redirected) + */ + redirected: boolean; + /** + * The **`url`** read-only property of the Response interface contains the URL of the response. The value of the url property will be the final URL obtained after any redirects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/url) + */ + url: string; + webSocket: WebSocket | null; + cf: any | undefined; + /** + * The **`type`** read-only property of the Response interface contains the type of the response. The type determines whether scripts are able to access the response body and headers. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/type) + */ + type: "default" | "error"; +} +interface ResponseInit { + status?: number; + statusText?: string; + headers?: HeadersInit; + cf?: any; + webSocket?: (WebSocket | null); + encodeBody?: "automatic" | "manual"; +} +type RequestInfo> = Request | string; +/** + * The **`Request`** interface of the Fetch API represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +declare var Request: { + prototype: Request; + new >(input: RequestInfo | URL, init?: RequestInit): Request; +}; +/** + * The **`Request`** interface of the Fetch API represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +interface Request> extends Body { + /** + * The **`clone()`** method of the Request interface creates a copy of the current Request object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/clone) + */ + clone(): Request; + /** + * The **`method`** read-only property of the Request interface contains the request's method (GET, POST, etc.) + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/method) + */ + method: string; + /** + * The **`url`** read-only property of the Request interface contains the URL of the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url) + */ + url: string; + /** + * The **`headers`** read-only property of the Request interface contains the Headers object associated with the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers) + */ + headers: Headers; + /** + * The **`redirect`** read-only property of the Request interface contains the mode for how redirects are handled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/redirect) + */ + redirect: string; + fetcher: Fetcher | null; + /** + * The read-only **`signal`** property of the Request interface returns the AbortSignal associated with the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) + */ + signal: AbortSignal; + cf?: Cf; + /** + * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity) + */ + integrity: string; + /** + * The **`keepalive`** read-only property of the Request interface contains the request's keepalive setting (true or false), which indicates whether the browser will keep the associated request alive if the page that initiated it is unloaded before the request is complete. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive) + */ + keepalive: boolean; + /** + * The **`cache`** read-only property of the Request interface contains the cache mode of the request. It controls how the request will interact with the browser's HTTP cache. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache) + */ + cache?: "no-store" | "no-cache"; +} +interface RequestInit { + /* A string to set request's method. */ + method?: string; + /* A Headers object, an object literal, or an array of two-item arrays to set request's headers. */ + headers?: HeadersInit; + /* A BodyInit object or null to set request's body. */ + body?: BodyInit | null; + /* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */ + redirect?: string; + fetcher?: (Fetcher | null); + cf?: Cf; + /* A string indicating how the request will interact with the browser's cache to set request's cache. */ + cache?: "no-store" | "no-cache"; + /* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */ + integrity?: string; + /* An AbortSignal to set request's signal. */ + signal?: (AbortSignal | null); + encodeResponseBody?: "automatic" | "manual"; +} +type Service Rpc.WorkerEntrypointBranded) | Rpc.WorkerEntrypointBranded | ExportedHandler | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? Fetcher> : T extends Rpc.WorkerEntrypointBranded ? Fetcher : T extends Exclude ? never : Fetcher; +type Fetcher = (T extends Rpc.EntrypointBranded ? Rpc.Provider : unknown) & { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + connect(address: SocketAddress | string, options?: SocketOptions): Socket; +}; +interface KVNamespaceListKey { + name: Key; + expiration?: number; + metadata?: Metadata; +} +type KVNamespaceListResult = { + list_complete: false; + keys: KVNamespaceListKey[]; + cursor: string; + cacheStatus: string | null; +} | { + list_complete: true; + keys: KVNamespaceListKey[]; + cacheStatus: string | null; +}; +interface KVNamespace { + get(key: Key, options?: Partial>): Promise; + get(key: Key, type: "text"): Promise; + get(key: Key, type: "json"): Promise; + get(key: Key, type: "arrayBuffer"): Promise; + get(key: Key, type: "stream"): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"text">): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"json">): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"arrayBuffer">): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"stream">): Promise; + get(key: Array, type: "text"): Promise>; + get(key: Array, type: "json"): Promise>; + get(key: Array, options?: Partial>): Promise>; + get(key: Array, options?: KVNamespaceGetOptions<"text">): Promise>; + get(key: Array, options?: KVNamespaceGetOptions<"json">): Promise>; + list(options?: KVNamespaceListOptions): Promise>; + put(key: Key, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVNamespacePutOptions): Promise; + getWithMetadata(key: Key, options?: Partial>): Promise>; + getWithMetadata(key: Key, type: "text"): Promise>; + getWithMetadata(key: Key, type: "json"): Promise>; + getWithMetadata(key: Key, type: "arrayBuffer"): Promise>; + getWithMetadata(key: Key, type: "stream"): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"text">): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"json">): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"arrayBuffer">): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"stream">): Promise>; + getWithMetadata(key: Array, type: "text"): Promise>>; + getWithMetadata(key: Array, type: "json"): Promise>>; + getWithMetadata(key: Array, options?: Partial>): Promise>>; + getWithMetadata(key: Array, options?: KVNamespaceGetOptions<"text">): Promise>>; + getWithMetadata(key: Array, options?: KVNamespaceGetOptions<"json">): Promise>>; + delete(key: Key): Promise; +} +interface KVNamespaceListOptions { + limit?: number; + prefix?: (string | null); + cursor?: (string | null); +} +interface KVNamespaceGetOptions { + type: Type; + cacheTtl?: number; +} +interface KVNamespacePutOptions { + expiration?: number; + expirationTtl?: number; + metadata?: (any | null); +} +interface KVNamespaceGetWithMetadataResult { + value: Value | null; + metadata: Metadata | null; + cacheStatus: string | null; +} +type QueueContentType = "text" | "bytes" | "json" | "v8"; +interface Queue { + metrics(): Promise; + send(message: Body, options?: QueueSendOptions): Promise; + sendBatch(messages: Iterable>, options?: QueueSendBatchOptions): Promise; +} +interface QueueSendMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface QueueSendMetadata { + metrics: QueueSendMetrics; +} +interface QueueSendResponse { + metadata: QueueSendMetadata; +} +interface QueueSendBatchMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface QueueSendBatchMetadata { + metrics: QueueSendBatchMetrics; +} +interface QueueSendBatchResponse { + metadata: QueueSendBatchMetadata; +} +interface QueueSendOptions { + contentType?: QueueContentType; + delaySeconds?: number; +} +interface QueueSendBatchOptions { + delaySeconds?: number; +} +interface MessageSendRequest { + body: Body; + contentType?: QueueContentType; + delaySeconds?: number; +} +interface QueueMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface MessageBatchMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface MessageBatchMetadata { + metrics: MessageBatchMetrics; +} +interface QueueRetryOptions { + delaySeconds?: number; +} +interface Message { + readonly id: string; + readonly timestamp: Date; + readonly body: Body; + readonly attempts: number; + retry(options?: QueueRetryOptions): void; + ack(): void; +} +interface QueueEvent extends ExtendableEvent { + readonly messages: readonly Message[]; + readonly queue: string; + readonly metadata: MessageBatchMetadata; + retryAll(options?: QueueRetryOptions): void; + ackAll(): void; +} +interface MessageBatch { + readonly messages: readonly Message[]; + readonly queue: string; + readonly metadata: MessageBatchMetadata; + retryAll(options?: QueueRetryOptions): void; + ackAll(): void; +} +interface R2Error extends Error { + readonly name: string; + readonly code: number; + readonly message: string; + readonly action: string; + readonly stack: any; +} +interface R2ListOptions { + limit?: number; + prefix?: string; + cursor?: string; + delimiter?: string; + startAfter?: string; + include?: ("httpMetadata" | "customMetadata")[]; +} +interface R2Bucket { + head(key: string): Promise; + get(key: string, options: R2GetOptions & { + onlyIf: R2Conditional | Headers; + }): Promise; + get(key: string, options?: R2GetOptions): Promise; + put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions & { + onlyIf: R2Conditional | Headers; + }): Promise; + put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise; + createMultipartUpload(key: string, options?: R2MultipartOptions): Promise; + resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload; + delete(keys: string | string[]): Promise; + list(options?: R2ListOptions): Promise; +} +interface R2MultipartUpload { + readonly key: string; + readonly uploadId: string; + uploadPart(partNumber: number, value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob, options?: R2UploadPartOptions): Promise; + abort(): Promise; + complete(uploadedParts: R2UploadedPart[]): Promise; +} +interface R2UploadedPart { + partNumber: number; + etag: string; +} +declare abstract class R2Object { + readonly key: string; + readonly version: string; + readonly size: number; + readonly etag: string; + readonly httpEtag: string; + readonly checksums: R2Checksums; + readonly uploaded: Date; + readonly httpMetadata?: R2HTTPMetadata; + readonly customMetadata?: Record; + readonly range?: R2Range; + readonly storageClass: string; + readonly ssecKeyMd5?: string; + writeHttpMetadata(headers: Headers): void; +} +interface R2ObjectBody extends R2Object { + get body(): ReadableStream; + get bodyUsed(): boolean; + arrayBuffer(): Promise; + bytes(): Promise; + text(): Promise; + json(): Promise; + blob(): Promise; +} +type R2Range = { + offset: number; + length?: number; +} | { + offset?: number; + length: number; +} | { + suffix: number; +}; +interface R2Conditional { + etagMatches?: string; + etagDoesNotMatch?: string; + uploadedBefore?: Date; + uploadedAfter?: Date; + secondsGranularity?: boolean; +} +interface R2GetOptions { + onlyIf?: (R2Conditional | Headers); + range?: (R2Range | Headers); + ssecKey?: (ArrayBuffer | string); +} +interface R2PutOptions { + onlyIf?: (R2Conditional | Headers); + httpMetadata?: (R2HTTPMetadata | Headers); + customMetadata?: Record; + md5?: ((ArrayBuffer | ArrayBufferView) | string); + sha1?: ((ArrayBuffer | ArrayBufferView) | string); + sha256?: ((ArrayBuffer | ArrayBufferView) | string); + sha384?: ((ArrayBuffer | ArrayBufferView) | string); + sha512?: ((ArrayBuffer | ArrayBufferView) | string); + storageClass?: string; + ssecKey?: (ArrayBuffer | string); +} +interface R2MultipartOptions { + httpMetadata?: (R2HTTPMetadata | Headers); + customMetadata?: Record; + storageClass?: string; + ssecKey?: (ArrayBuffer | string); +} +interface R2Checksums { + readonly md5?: ArrayBuffer; + readonly sha1?: ArrayBuffer; + readonly sha256?: ArrayBuffer; + readonly sha384?: ArrayBuffer; + readonly sha512?: ArrayBuffer; + toJSON(): R2StringChecksums; +} +interface R2StringChecksums { + md5?: string; + sha1?: string; + sha256?: string; + sha384?: string; + sha512?: string; +} +interface R2HTTPMetadata { + contentType?: string; + contentLanguage?: string; + contentDisposition?: string; + contentEncoding?: string; + cacheControl?: string; + cacheExpiry?: Date; +} +type R2Objects = { + objects: R2Object[]; + delimitedPrefixes: string[]; +} & ({ + truncated: true; + cursor: string; +} | { + truncated: false; +}); +interface R2UploadPartOptions { + ssecKey?: (ArrayBuffer | string); +} +declare abstract class ScheduledEvent extends ExtendableEvent { + readonly scheduledTime: number; + readonly cron: string; + noRetry(): void; +} +interface ScheduledController { + readonly scheduledTime: number; + readonly cron: string; + noRetry(): void; +} +interface QueuingStrategy { + highWaterMark?: (number | bigint); + size?: (chunk: T) => number | bigint; +} +interface UnderlyingSink { + type?: string; + start?: (controller: WritableStreamDefaultController) => void | Promise; + write?: (chunk: W, controller: WritableStreamDefaultController) => void | Promise; + abort?: (reason: any) => void | Promise; + close?: () => void | Promise; +} +interface UnderlyingByteSource { + type: "bytes"; + autoAllocateChunkSize?: number; + start?: (controller: ReadableByteStreamController) => void | Promise; + pull?: (controller: ReadableByteStreamController) => void | Promise; + cancel?: (reason: any) => void | Promise; +} +interface UnderlyingSource { + type?: "" | undefined; + start?: (controller: ReadableStreamDefaultController) => void | Promise; + pull?: (controller: ReadableStreamDefaultController) => void | Promise; + cancel?: (reason: any) => void | Promise; + expectedLength?: (number | bigint); +} +interface Transformer { + readableType?: string; + writableType?: string; + start?: (controller: TransformStreamDefaultController) => void | Promise; + transform?: (chunk: I, controller: TransformStreamDefaultController) => void | Promise; + flush?: (controller: TransformStreamDefaultController) => void | Promise; + cancel?: (reason: any) => void | Promise; + expectedLength?: number; +} +interface StreamPipeOptions { + preventAbort?: boolean; + preventCancel?: boolean; + /** + * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + * + * Errors and closures of the source and destination streams propagate as follows: + * + * An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination. + * + * An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source. + * + * When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error. + * + * If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source. + * + * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. + */ + preventClose?: boolean; + signal?: AbortSignal; +} +type ReadableStreamReadResult = { + done: false; + value: R; +} | { + done: true; + value?: undefined; +}; +/** + * The **`ReadableStream`** interface of the Streams API represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +interface ReadableStream { + /** + * The **`locked`** read-only property of the ReadableStream interface returns whether or not the readable stream is locked to a reader. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) + */ + get locked(): boolean; + /** + * The **`cancel()`** method of the ReadableStream interface returns a Promise that resolves when the stream is canceled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel) + */ + cancel(reason?: any): Promise; + /** + * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it. While the stream is locked, no other reader can be acquired until this one is released. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) + */ + getReader(): ReadableStreamDefaultReader; + /** + * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it. While the stream is locked, no other reader can be acquired until this one is released. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) + */ + getReader(options: ReadableStreamGetReaderOptions): ReadableStreamBYOBReader; + /** + * The **`pipeThrough()`** method of the ReadableStream interface provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough) + */ + pipeThrough(transform: ReadableWritablePair, options?: StreamPipeOptions): ReadableStream; + /** + * The **`pipeTo()`** method of the ReadableStream interface pipes the current ReadableStream to a given WritableStream and returns a Promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo) + */ + pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise; + /** + * The **`tee()`** method of the ReadableStream interface tees the current readable stream, returning a two-element array containing the two resulting branches as new ReadableStream instances. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee) + */ + tee(): [ + ReadableStream, + ReadableStream + ]; + values(options?: ReadableStreamValuesOptions): AsyncIterableIterator; + [Symbol.asyncIterator](options?: ReadableStreamValuesOptions): AsyncIterableIterator; +} +/** + * The **`ReadableStream`** interface of the Streams API represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +declare const ReadableStream: { + prototype: ReadableStream; + new (underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy): ReadableStream; + new (underlyingSource?: UnderlyingSource, strategy?: QueuingStrategy): ReadableStream; +}; +/** + * The **`ReadableStreamDefaultReader`** interface of the Streams API represents a default reader that can be used to read stream data supplied from a network (such as a fetch request). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader) + */ +declare class ReadableStreamDefaultReader { + constructor(stream: ReadableStream); + get closed(): Promise; + cancel(reason?: any): Promise; + /** + * The **`read()`** method of the ReadableStreamDefaultReader interface returns a Promise providing access to the next chunk in the stream's internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/read) + */ + read(): Promise>; + /** + * The **`releaseLock()`** method of the ReadableStreamDefaultReader interface releases the reader's lock on the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/releaseLock) + */ + releaseLock(): void; +} +/** + * The **`ReadableStreamBYOBReader`** interface of the Streams API defines a reader for a ReadableStream that supports zero-copy reading from an underlying byte source. It is used for efficient copying from underlying sources where the data is delivered as an "anonymous" sequence of bytes, such as files. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader) + */ +declare class ReadableStreamBYOBReader { + constructor(stream: ReadableStream); + get closed(): Promise; + cancel(reason?: any): Promise; + /** + * The **`read()`** method of the ReadableStreamBYOBReader interface is used to read data into a view on a user-supplied buffer from an associated readable byte stream. A request for data will be satisfied from the stream's internal queues if there is any data present. If the stream queues are empty, the request may be supplied as a zero-copy transfer from the underlying byte source. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) + */ + read(view: T): Promise>; + /** + * The **`releaseLock()`** method of the ReadableStreamBYOBReader interface releases the reader's lock on the stream. After the lock is released, the reader is no longer active. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) + */ + releaseLock(): void; + readAtLeast(minElements: number, view: T): Promise>; +} +interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions { + min?: number; +} +interface ReadableStreamGetReaderOptions { + /** + * Creates a ReadableStreamBYOBReader and locks the stream to the new reader. + * + * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation. + */ + mode: "byob"; +} +/** + * The **`ReadableStreamBYOBRequest`** interface of the Streams API represents a "pull request" for data from an underlying source that will made as a zero-copy transfer to a consumer (bypassing the stream's internal queues). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest) + */ +declare abstract class ReadableStreamBYOBRequest { + /** + * The **`view`** getter property of the ReadableStreamBYOBRequest interface returns the current view. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view) + */ + get view(): Uint8Array | null; + /** + * The **`respond()`** method of the ReadableStreamBYOBRequest interface is used to signal to the associated readable byte stream that the specified number of bytes were written into the ReadableStreamBYOBRequest.view. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond) + */ + respond(bytesWritten: number): void; + /** + * The **`respondWithNewView()`** method of the ReadableStreamBYOBRequest interface specifies a new view that the consumer of the associated readable byte stream should write to instead of ReadableStreamBYOBRequest.view. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView) + */ + respondWithNewView(view: ArrayBuffer | ArrayBufferView): void; + get atLeast(): number | null; +} +/** + * The **`ReadableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a ReadableStream's state and internal queue. Default controllers are for streams that are not byte streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController) + */ +declare abstract class ReadableStreamDefaultController { + /** + * The **`desiredSize`** read-only property of the ReadableStreamDefaultController interface returns the desired size required to fill the stream's internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`close()`** method of the ReadableStreamDefaultController interface closes the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/close) + */ + close(): void; + /** + * The **`enqueue()`** method of the ReadableStreamDefaultController interface enqueues a given chunk in the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue) + */ + enqueue(chunk?: R): void; + /** + * The **`error()`** method of the ReadableStreamDefaultController interface causes any future interactions with the associated stream to error. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/error) + */ + error(reason: any): void; +} +/** + * The **`ReadableByteStreamController`** interface of the Streams API represents a controller for a readable byte stream. It allows control of the state and internal queue of a ReadableStream with an underlying byte source, and enables efficient zero-copy transfer of data from the underlying source to a consumer when the stream's internal queue is empty. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController) + */ +declare abstract class ReadableByteStreamController { + /** + * The **`byobRequest`** read-only property of the ReadableByteStreamController interface returns the current BYOB request, or null if there are no pending requests. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/byobRequest) + */ + get byobRequest(): ReadableStreamBYOBRequest | null; + /** + * The **`desiredSize`** read-only property of the ReadableByteStreamController interface returns the number of bytes required to fill the stream's internal queue to its "desired size". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`close()`** method of the ReadableByteStreamController interface closes the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/close) + */ + close(): void; + /** + * The **`enqueue()`** method of the ReadableByteStreamController interface enqueues a given chunk on the associated readable byte stream (the chunk is transferred into the stream's internal queues). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/enqueue) + */ + enqueue(chunk: ArrayBuffer | ArrayBufferView): void; + /** + * The **`error()`** method of the ReadableByteStreamController interface causes any future interactions with the associated stream to error with the specified reason. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/error) + */ + error(reason: any): void; +} +/** + * The **`WritableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a WritableStream's state. When constructing a WritableStream, the underlying sink is given a corresponding WritableStreamDefaultController instance to manipulate. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController) + */ +declare abstract class WritableStreamDefaultController { + /** + * The read-only **`signal`** property of the WritableStreamDefaultController interface returns the AbortSignal associated with the controller. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal) + */ + get signal(): AbortSignal; + /** + * The **`error()`** method of the WritableStreamDefaultController interface causes any future interactions with the associated stream to error. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error) + */ + error(reason?: any): void; +} +/** + * The **`TransformStreamDefaultController`** interface of the Streams API provides methods to manipulate the associated ReadableStream and WritableStream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController) + */ +declare abstract class TransformStreamDefaultController { + /** + * The **`desiredSize`** read-only property of the TransformStreamDefaultController interface returns the desired size to fill the queue of the associated ReadableStream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`enqueue()`** method of the TransformStreamDefaultController interface enqueues the given chunk in the readable side of the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue) + */ + enqueue(chunk?: O): void; + /** + * The **`error()`** method of the TransformStreamDefaultController interface errors both sides of the stream. Any further interactions with it will fail with the given error message, and any chunks in the queue will be discarded. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/error) + */ + error(reason: any): void; + /** + * The **`terminate()`** method of the TransformStreamDefaultController interface closes the readable side and errors the writable side of the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate) + */ + terminate(): void; +} +interface ReadableWritablePair { + readable: ReadableStream; + /** + * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + */ + writable: WritableStream; +} +/** + * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink. This object comes with built-in backpressure and queuing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream) + */ +declare class WritableStream { + constructor(underlyingSink?: UnderlyingSink, queuingStrategy?: QueuingStrategy); + /** + * The **`locked`** read-only property of the WritableStream interface returns a boolean indicating whether the WritableStream is locked to a writer. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/locked) + */ + get locked(): boolean; + /** + * The **`abort()`** method of the WritableStream interface aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/abort) + */ + abort(reason?: any): Promise; + /** + * The **`close()`** method of the WritableStream interface closes the associated stream. All chunks written before this method is called are sent before the returned promise is fulfilled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/close) + */ + close(): Promise; + /** + * The **`getWriter()`** method of the WritableStream interface returns a new instance of WritableStreamDefaultWriter and locks the stream to that instance. While the stream is locked, no other writer can be acquired until this one is released. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/getWriter) + */ + getWriter(): WritableStreamDefaultWriter; +} +/** + * The **`WritableStreamDefaultWriter`** interface of the Streams API is the object returned by WritableStream.getWriter() and once created locks the writer to the WritableStream ensuring that no other streams can write to the underlying sink. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter) + */ +declare class WritableStreamDefaultWriter { + constructor(stream: WritableStream); + /** + * The **`closed`** read-only property of the WritableStreamDefaultWriter interface returns a Promise that fulfills if the stream becomes closed, or rejects if the stream errors or the writer's lock is released. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed) + */ + get closed(): Promise; + /** + * The **`ready`** read-only property of the WritableStreamDefaultWriter interface returns a Promise that resolves when the desired size of the stream's internal queue transitions from non-positive to positive, signaling that it is no longer applying backpressure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready) + */ + get ready(): Promise; + /** + * The **`desiredSize`** read-only property of the WritableStreamDefaultWriter interface returns the desired size required to fill the stream's internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`abort()`** method of the WritableStreamDefaultWriter interface aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort) + */ + abort(reason?: any): Promise; + /** + * The **`close()`** method of the WritableStreamDefaultWriter interface closes the associated writable stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close) + */ + close(): Promise; + /** + * The **`write()`** method of the WritableStreamDefaultWriter interface writes a passed chunk of data to a WritableStream and its underlying sink, then returns a Promise that resolves to indicate the success or failure of the write operation. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write) + */ + write(chunk?: W): Promise; + /** + * The **`releaseLock()`** method of the WritableStreamDefaultWriter interface releases the writer's lock on the corresponding stream. After the lock is released, the writer is no longer active. If the associated stream is errored when the lock is released, the writer will appear errored in the same way from now on; otherwise, the writer will appear closed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock) + */ + releaseLock(): void; +} +/** + * The **`TransformStream`** interface of the Streams API represents a concrete implementation of the pipe chain transform stream concept. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream) + */ +declare class TransformStream { + constructor(transformer?: Transformer, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy); + /** + * The **`readable`** read-only property of the TransformStream interface returns the ReadableStream instance controlled by this TransformStream. This stream emits the transformed output data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/readable) + */ + get readable(): ReadableStream; + /** + * The **`writable`** read-only property of the TransformStream interface returns the WritableStream instance controlled by this TransformStream. This stream accepts input data that will be transformed and emitted to the readable stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/writable) + */ + get writable(): WritableStream; +} +declare class FixedLengthStream extends IdentityTransformStream { + constructor(expectedLength: number | bigint, queuingStrategy?: IdentityTransformStreamQueuingStrategy); +} +declare class IdentityTransformStream extends TransformStream { + constructor(queuingStrategy?: IdentityTransformStreamQueuingStrategy); +} +interface IdentityTransformStreamQueuingStrategy { + highWaterMark?: (number | bigint); +} +interface ReadableStreamValuesOptions { + preventCancel?: boolean; +} +/** + * The **`CompressionStream`** interface of the Compression Streams API compresses a stream of data. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CompressionStream) + */ +declare class CompressionStream extends TransformStream { + constructor(format: "gzip" | "deflate" | "deflate-raw"); +} +/** + * The **`DecompressionStream`** interface of the Compression Streams API decompresses a stream of data. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DecompressionStream) + */ +declare class DecompressionStream extends TransformStream { + constructor(format: "gzip" | "deflate" | "deflate-raw"); +} +/** + * The **`TextEncoderStream`** interface of the Encoding API converts a stream of strings into bytes in the UTF-8 encoding. It is the streaming equivalent of TextEncoder. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoderStream) + */ +declare class TextEncoderStream extends TransformStream { + constructor(); + get encoding(): string; +} +/** + * The **`TextDecoderStream`** interface of the Encoding API converts a stream of text in a binary encoding, such as UTF-8 etc., to a stream of strings. It is the streaming equivalent of TextDecoder. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoderStream) + */ +declare class TextDecoderStream extends TransformStream { + constructor(label?: string, options?: TextDecoderStreamTextDecoderStreamInit); + get encoding(): string; + get fatal(): boolean; + get ignoreBOM(): boolean; +} +interface TextDecoderStreamTextDecoderStreamInit { + fatal?: boolean; + ignoreBOM?: boolean; +} +/** + * The **`ByteLengthQueuingStrategy`** interface of the Streams API provides a built-in byte length queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy) + */ +declare class ByteLengthQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit); + /** + * The read-only **`ByteLengthQueuingStrategy.highWaterMark`** property returns the total number of bytes that can be contained in the internal queue before backpressure is applied. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/highWaterMark) + */ + get highWaterMark(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/size) */ + get size(): (chunk?: any) => number; +} +/** + * The **`CountQueuingStrategy`** interface of the Streams API provides a built-in chunk counting queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy) + */ +declare class CountQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit); + /** + * The read-only **`CountQueuingStrategy.highWaterMark`** property returns the total number of chunks that can be contained in the internal queue before backpressure is applied. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/highWaterMark) + */ + get highWaterMark(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/size) */ + get size(): (chunk?: any) => number; +} +interface QueuingStrategyInit { + /** + * Creates a new ByteLengthQueuingStrategy with the provided high water mark. + * + * Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw. + */ + highWaterMark: number; +} +interface TracePreviewInfo { + id: string; + slug: string; + name: string; +} +interface ScriptVersion { + id?: string; + tag?: string; + message?: string; +} +declare abstract class TailEvent extends ExtendableEvent { + readonly events: TraceItem[]; + readonly traces: TraceItem[]; +} +interface TraceItem { + readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemConnectEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null; + readonly eventTimestamp: number | null; + readonly logs: TraceLog[]; + readonly exceptions: TraceException[]; + readonly diagnosticsChannelEvents: TraceDiagnosticChannelEvent[]; + readonly scriptName: string | null; + readonly entrypoint?: string; + readonly scriptVersion?: ScriptVersion; + readonly dispatchNamespace?: string; + readonly scriptTags?: string[]; + readonly tailAttributes?: Record; + readonly preview?: TracePreviewInfo; + readonly durableObjectId?: string; + readonly outcome: string; + readonly executionModel: string; + readonly truncated: boolean; + readonly cpuTime: number; + readonly wallTime: number; +} +interface TraceItemAlarmEventInfo { + readonly scheduledTime: Date; +} +interface TraceItemConnectEventInfo { +} +interface TraceItemCustomEventInfo { +} +interface TraceItemScheduledEventInfo { + readonly scheduledTime: number; + readonly cron: string; +} +interface TraceItemQueueEventInfo { + readonly queue: string; + readonly batchSize: number; +} +interface TraceItemEmailEventInfo { + readonly mailFrom: string; + readonly rcptTo: string; + readonly rawSize: number; +} +interface TraceItemTailEventInfo { + readonly consumedEvents: TraceItemTailEventInfoTailItem[]; +} +interface TraceItemTailEventInfoTailItem { + readonly scriptName: string | null; +} +interface TraceItemFetchEventInfo { + readonly response?: TraceItemFetchEventInfoResponse; + readonly request: TraceItemFetchEventInfoRequest; +} +interface TraceItemFetchEventInfoRequest { + readonly cf?: any; + readonly headers: Record; + readonly method: string; + readonly url: string; + getUnredacted(): TraceItemFetchEventInfoRequest; +} +interface TraceItemFetchEventInfoResponse { + readonly status: number; +} +interface TraceItemJsRpcEventInfo { + readonly rpcMethod: string; +} +interface TraceItemHibernatableWebSocketEventInfo { + readonly getWebSocketEvent: TraceItemHibernatableWebSocketEventInfoMessage | TraceItemHibernatableWebSocketEventInfoClose | TraceItemHibernatableWebSocketEventInfoError; +} +interface TraceItemHibernatableWebSocketEventInfoMessage { + readonly webSocketEventType: string; +} +interface TraceItemHibernatableWebSocketEventInfoClose { + readonly webSocketEventType: string; + readonly code: number; + readonly wasClean: boolean; +} +interface TraceItemHibernatableWebSocketEventInfoError { + readonly webSocketEventType: string; +} +interface TraceLog { + readonly timestamp: number; + readonly level: string; + readonly message: any; + readonly errorInfo?: (TraceLogErrorInfo | null)[]; +} +interface TraceLogErrorInfo { + name: string; + message: string; + stack?: string; +} +interface TraceException { + readonly timestamp: number; + readonly message: string; + readonly name: string; + readonly stack?: string; +} +interface TraceDiagnosticChannelEvent { + readonly timestamp: number; + readonly channel: string; + readonly message: any; +} +interface TraceMetrics { + readonly cpuTime: number; + readonly wallTime: number; +} +interface UnsafeTraceMetrics { + fromTrace(item: TraceItem): TraceMetrics; +} +/** + * The **`URL`** interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL) + */ +declare class URL { + constructor(url: string | URL, base?: string | URL); + /** + * The **`origin`** read-only property of the URL interface returns a string containing the Unicode serialization of the origin of the represented URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin) + */ + get origin(): string; + /** + * The **`href`** property of the URL interface is a string containing the whole URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) + */ + get href(): string; + /** + * The **`href`** property of the URL interface is a string containing the whole URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) + */ + set href(value: string); + /** + * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final ":". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) + */ + get protocol(): string; + /** + * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final ":". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) + */ + set protocol(value: string); + /** + * The **`username`** property of the URL interface is a string containing the username component of the URL. If the URL does not have a username, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) + */ + get username(): string; + /** + * The **`username`** property of the URL interface is a string containing the username component of the URL. If the URL does not have a username, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) + */ + set username(value: string); + /** + * The **`password`** property of the URL interface is a string containing the password component of the URL. If the URL does not have a password, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) + */ + get password(): string; + /** + * The **`password`** property of the URL interface is a string containing the password component of the URL. If the URL does not have a password, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) + */ + set password(value: string); + /** + * The **`host`** property of the URL interface is a string containing the host, which is the hostname, and then, if the port of the URL is nonempty, a ":", followed by the port of the URL. If the URL does not have a hostname, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) + */ + get host(): string; + /** + * The **`host`** property of the URL interface is a string containing the host, which is the hostname, and then, if the port of the URL is nonempty, a ":", followed by the port of the URL. If the URL does not have a hostname, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) + */ + set host(value: string); + /** + * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL. If the URL does not have a hostname, this property contains an empty string, "". IPv4 and IPv6 addresses are normalized, such as stripping leading zeros, and domain names are converted to IDN. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) + */ + get hostname(): string; + /** + * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL. If the URL does not have a hostname, this property contains an empty string, "". IPv4 and IPv6 addresses are normalized, such as stripping leading zeros, and domain names are converted to IDN. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) + */ + set hostname(value: string); + /** + * The **`port`** property of the URL interface is a string containing the port number of the URL. If the port is the default for the protocol (80 for ws: and http:, 443 for wss: and https:, and 21 for ftp:), this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) + */ + get port(): string; + /** + * The **`port`** property of the URL interface is a string containing the port number of the URL. If the port is the default for the protocol (80 for ws: and http:, 443 for wss: and https:, and 21 for ftp:), this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) + */ + set port(value: string); + /** + * The **`pathname`** property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) + */ + get pathname(): string; + /** + * The **`pathname`** property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) + */ + set pathname(value: string); + /** + * The **`search`** property of the URL interface is a search string, also called a query string, that is a string containing a "?" followed by the parameters of the URL. If the URL does not have a search query, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) + */ + get search(): string; + /** + * The **`search`** property of the URL interface is a search string, also called a query string, that is a string containing a "?" followed by the parameters of the URL. If the URL does not have a search query, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) + */ + set search(value: string); + /** + * The **`hash`** property of the URL interface is a string containing a "#" followed by the fragment identifier of the URL. If the URL does not have a fragment identifier, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) + */ + get hash(): string; + /** + * The **`hash`** property of the URL interface is a string containing a "#" followed by the fragment identifier of the URL. If the URL does not have a fragment identifier, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) + */ + set hash(value: string); + /** + * The **`searchParams`** read-only property of the URL interface returns a URLSearchParams object allowing access to the GET decoded query arguments contained in the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams) + */ + get searchParams(): URLSearchParams; + /** + * The **`toJSON()`** method of the URL interface returns a string containing a serialized version of the URL, although in practice it seems to have the same effect as URL.toString(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON) + */ + toJSON(): string; + /*function toString() { [native code] }*/ + toString(): string; + /** + * The **`URL.canParse()`** static method of the URL interface returns a boolean indicating whether or not an absolute URL, or a relative URL combined with a base URL, are parsable and valid. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) + */ + static canParse(url: string, base?: string): boolean; + /** + * The **`URL.parse()`** static method of the URL interface returns a newly created URL object representing the URL defined by the parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/parse_static) + */ + static parse(url: string, base?: string): URL | null; + /** + * The **`createObjectURL()`** static method of the URL interface creates a string containing a blob URL pointing to the object given in the parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static) + */ + static createObjectURL(object: File | Blob): string; + /** + * The **`revokeObjectURL()`** static method of the URL interface releases an existing object URL which was previously created by calling URL.createObjectURL(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static) + */ + static revokeObjectURL(object_url: string): void; +} +/** + * The **`URLSearchParams`** interface defines utility methods to work with the query string of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams) + */ +declare class URLSearchParams { + constructor(init?: (Iterable> | Record | string)); + /** + * The **`size`** read-only property of the URLSearchParams interface indicates the total number of search parameter entries. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) + */ + get size(): number; + /** + * The **`append()`** method of the URLSearchParams interface appends a specified key/value pair as a new search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append) + */ + append(name: string, value: string): void; + /** + * The **`delete()`** method of the URLSearchParams interface deletes specified parameters and their associated value(s) from the list of all search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete) + */ + delete(name: string, value?: string): void; + /** + * The **`get()`** method of the URLSearchParams interface returns the first value associated to the given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) + */ + get(name: string): string | null; + /** + * The **`getAll()`** method of the URLSearchParams interface returns all the values associated with a given search parameter as an array. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) + */ + getAll(name: string): string[]; + /** + * The **`has()`** method of the URLSearchParams interface returns a boolean value that indicates whether the specified parameter is in the search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) + */ + has(name: string, value?: string): boolean; + /** + * The **`set()`** method of the URLSearchParams interface sets the value associated with a given search parameter to the given value. If there were several matching values, this method deletes the others. If the search parameter doesn't exist, this method creates it. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) + */ + set(name: string, value: string): void; + /** + * The **`URLSearchParams.sort()`** method sorts all key/value pairs contained in this object in place and returns undefined. Key/value pairs are sorted by the values of the UTF-16 code units of the keys. This method uses a stable sorting algorithm (i.e., the relative order between key/value pairs with equal keys will be preserved). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) + */ + sort(): void; + entries(): IterableIterator<[ + key: string, + value: string + ]>; + keys(): IterableIterator; + values(): IterableIterator; + forEach(callback: (this: This, value: string, key: string, parent: URLSearchParams) => void, thisArg?: This): void; + /*function toString() { [native code] }*/ + toString(): string; + [Symbol.iterator](): IterableIterator<[ + key: string, + value: string + ]>; +} +/** + * The **`URLPattern`** interface of the URL Pattern API matches URLs or parts of URLs against a pattern. The pattern can contain capturing groups that extract parts of the matched URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern) + */ +declare class URLPattern { + constructor(input?: (string | URLPatternInit), baseURL?: (string | URLPatternOptions), patternOptions?: URLPatternOptions); + /** + * The **`protocol`** read-only property of the URLPattern interface is a string containing the pattern used to match the protocol part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/protocol) + */ + get protocol(): string; + /** + * The **`username`** read-only property of the URLPattern interface is a string containing the pattern used to match the username part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/username) + */ + get username(): string; + /** + * The **`password`** read-only property of the URLPattern interface is a string containing the pattern used to match the password part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/password) + */ + get password(): string; + /** + * The **`hostname`** read-only property of the URLPattern interface is a string containing the pattern used to match the hostname part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/hostname) + */ + get hostname(): string; + /** + * The **`port`** read-only property of the URLPattern interface is a string containing the pattern used to match the port part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/port) + */ + get port(): string; + /** + * The **`pathname`** read-only property of the URLPattern interface is a string containing the pattern used to match the pathname part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/pathname) + */ + get pathname(): string; + /** + * The **`search`** read-only property of the URLPattern interface is a string containing the pattern used to match the search part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/search) + */ + get search(): string; + /** + * The **`hash`** read-only property of the URLPattern interface is a string containing the pattern used to match the fragment part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/hash) + */ + get hash(): string; + /** + * The **`hasRegExpGroups`** read-only property of the URLPattern interface is a boolean indicating whether or not any of the URLPattern components contain regular expression capturing groups. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/hasRegExpGroups) + */ + get hasRegExpGroups(): boolean; + /** + * The **`test()`** method of the URLPattern interface takes a URL string or object of URL parts, and returns a boolean indicating if the given input matches the current pattern. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/test) + */ + test(input?: (string | URLPatternInit), baseURL?: string): boolean; + /** + * The **`exec()`** method of the URLPattern interface takes a URL or object of URL parts, and returns either an object containing the results of matching the URL to the pattern, or null if the URL does not match the pattern. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/exec) + */ + exec(input?: (string | URLPatternInit), baseURL?: string): URLPatternResult | null; +} +interface URLPatternInit { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} +interface URLPatternComponentResult { + input: string; + groups: Record; +} +interface URLPatternResult { + inputs: (string | URLPatternInit)[]; + protocol: URLPatternComponentResult; + username: URLPatternComponentResult; + password: URLPatternComponentResult; + hostname: URLPatternComponentResult; + port: URLPatternComponentResult; + pathname: URLPatternComponentResult; + search: URLPatternComponentResult; + hash: URLPatternComponentResult; +} +interface URLPatternOptions { + ignoreCase?: boolean; +} +/** + * A **`CloseEvent`** is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent) + */ +declare class CloseEvent extends Event { + constructor(type: string, initializer?: CloseEventInit); + /** + * The **`code`** read-only property of the CloseEvent interface returns a WebSocket connection close code indicating the reason the connection was closed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code) + */ + readonly code: number; + /** + * The **`reason`** read-only property of the CloseEvent interface returns the WebSocket connection close reason the server gave for closing the connection; that is, a concise human-readable prose explanation for the closure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason) + */ + readonly reason: string; + /** + * The **`wasClean`** read-only property of the CloseEvent interface returns true if the connection closed cleanly. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean) + */ + readonly wasClean: boolean; +} +interface CloseEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + code?: number; + reason?: string; + wasClean?: boolean; +} +type WebSocketEventMap = { + close: CloseEvent; + message: MessageEvent; + open: Event; + error: ErrorEvent; +}; +/** + * The **`WebSocket`** object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +declare var WebSocket: { + prototype: WebSocket; + new (url: string, protocols?: (string[] | string)): WebSocket; + readonly READY_STATE_CONNECTING: number; + readonly CONNECTING: number; + readonly READY_STATE_OPEN: number; + readonly OPEN: number; + readonly READY_STATE_CLOSING: number; + readonly CLOSING: number; + readonly READY_STATE_CLOSED: number; + readonly CLOSED: number; +}; +/** + * The **`WebSocket`** object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +interface WebSocket extends EventTarget { + accept(options?: WebSocketAcceptOptions): void; + /** + * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of bufferedAmount by the number of bytes needed to contain the data. If the data can't be sent (for example, because it needs to be buffered but the buffer is full), the socket is closed automatically. The browser will throw an exception if you call send() when the connection is in the CONNECTING state. If you call send() when the connection is in the CLOSING or CLOSED states, the browser will silently discard the data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/send) + */ + send(message: (ArrayBuffer | ArrayBufferView) | string): void; + /** + * The **`WebSocket.close()`** method closes the WebSocket connection or connection attempt, if any. If the connection is already CLOSED, this method does nothing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/close) + */ + close(code?: number, reason?: string): void; + serializeAttachment(attachment: any): void; + deserializeAttachment(): any | null; + /** + * The **`WebSocket.readyState`** read-only property returns the current state of the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/readyState) + */ + readyState: number; + /** + * The **`WebSocket.url`** read-only property returns the absolute URL of the WebSocket as resolved by the constructor. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/url) + */ + url: string | null; + /** + * The **`WebSocket.protocol`** read-only property returns the name of the sub-protocol the server selected; this will be one of the strings specified in the protocols parameter when creating the WebSocket object, or the empty string if no connection is established. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/protocol) + */ + protocol: string | null; + /** + * The **`WebSocket.extensions`** read-only property returns the extensions selected by the server. This is currently only the empty string or a list of extensions as negotiated by the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) + */ + extensions: string | null; + /** + * The **`WebSocket.binaryType`** property controls the type of binary data being received over the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) + */ + binaryType: "blob" | "arraybuffer"; +} +interface WebSocketAcceptOptions { + /** + * When set to `true`, receiving a server-initiated WebSocket Close frame will not + * automatically send a reciprocal Close frame, leaving the connection in a half-open + * state. This is useful for proxying scenarios where you need to coordinate closing + * both sides independently. Defaults to `false` when the + * `no_web_socket_half_open_by_default` compatibility flag is enabled. + */ + allowHalfOpen?: boolean; +} +declare const WebSocketPair: { + new (): { + 0: WebSocket; + 1: WebSocket; + }; +}; +interface SqlStorage { + exec>(query: string, ...bindings: any[]): SqlStorageCursor; + get databaseSize(): number; + Cursor: typeof SqlStorageCursor; + Statement: typeof SqlStorageStatement; +} +declare abstract class SqlStorageStatement { +} +type SqlStorageValue = ArrayBuffer | string | number | null; +declare abstract class SqlStorageCursor> { + next(): IteratorResult; + toArray(): T[]; + one(): T; + raw(): IterableIterator; + columnNames: string[]; + get rowsRead(): number; + get rowsWritten(): number; + [Symbol.iterator](): IterableIterator; +} +interface Socket { + get readable(): ReadableStream; + get writable(): WritableStream; + get closed(): Promise; + get opened(): Promise; + get upgraded(): boolean; + get secureTransport(): "on" | "off" | "starttls"; + close(): Promise; + startTls(options?: TlsOptions): Socket; +} +interface SocketOptions { + secureTransport?: string; + allowHalfOpen: boolean; + highWaterMark?: (number | bigint); +} +interface SocketAddress { + hostname: string; + port: number; +} +interface TlsOptions { + expectedServerHostname?: string; +} +interface SocketInfo { + remoteAddress?: string; + localAddress?: string; +} +/** + * The **`EventSource`** interface is web content's interface to server-sent events. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource) + */ +declare class EventSource extends EventTarget { + constructor(url: string, init?: EventSourceEventSourceInit); + /** + * The **`close()`** method of the EventSource interface closes the connection, if one is made, and sets the EventSource.readyState attribute to 2 (closed). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close) + */ + close(): void; + /** + * The **`url`** read-only property of the EventSource interface returns a string representing the URL of the source. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url) + */ + get url(): string; + /** + * The **`withCredentials`** read-only property of the EventSource interface returns a boolean value indicating whether the EventSource object was instantiated with CORS credentials set. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials) + */ + get withCredentials(): boolean; + /** + * The **`readyState`** read-only property of the EventSource interface returns a number representing the state of the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState) + */ + get readyState(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + get onopen(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + set onopen(value: any | null); + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + get onmessage(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + set onmessage(value: any | null); + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + get onerror(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + set onerror(value: any | null); + static readonly CONNECTING: number; + static readonly OPEN: number; + static readonly CLOSED: number; + static from(stream: ReadableStream): EventSource; +} +interface EventSourceEventSourceInit { + withCredentials?: boolean; + fetcher?: Fetcher; +} +interface ExecOutput { + readonly stdout: ArrayBuffer; + readonly stderr: ArrayBuffer; + readonly exitCode: number; +} +interface ContainerExecOptions { + cwd?: string; + env?: Record; + user?: string; + signal?: AbortSignal; + pty?: boolean | ContainerExecPtyOptions; + stdin?: ReadableStream | "pipe"; + stdout?: "pipe" | "ignore"; + stderr?: "pipe" | "ignore" | "combined"; +} +interface ContainerExecPtyOptions { + cols?: number; + rows?: number; +} +interface ExecProcess { + readonly stdin: WritableStream | null; + readonly stdout: ReadableStream | null; + readonly stderr: ReadableStream | null; + readonly pid: number; + readonly isPty: boolean; + readonly exitCode: Promise; + output(): Promise; + kill(signal?: number): void; + resize(cols: number, rows: number): void; +} +interface Container { + get running(): boolean; + get images(): Record; + start(options?: ContainerStartupOptions): void; + monitor(): Promise; + destroy(error?: any): Promise; + signal(signo: number): void; + getTcpPort(port: number): Fetcher; + setInactivityTimeout(durationMs: number | bigint): Promise; + interceptOutboundHttp(addr: string, binding: Fetcher): Promise; + interceptAllOutboundHttp(binding: Fetcher): Promise; + snapshotContainer(options?: ContainerSnapshotOptions): Promise; + interceptOutboundHttps(addr: string, binding: Fetcher): Promise; + exec(cmd: string[], options?: ContainerExecOptions): Promise; + inspect(): Promise; +} +interface ContainerDirectorySnapshot { + id: string; + size: number; + dir: string; + name?: string; +} +type ContainerDirectorySnapshotRestoreParams = { + snapshot: ContainerDirectorySnapshot; + mountPoint?: string; +} | { + snapshot?: undefined; + mountPoint: string; +}; +interface ContainerSnapshot { + id: string; + size: number; + name?: string; +} +interface ContainerSnapshotRestoreParams { + id: string; +} +interface ContainerSnapshotOptions { + name?: string; +} +type ContainerStartupOptions = { + entrypoint?: string[]; + enableInternet: boolean; + env?: Record; + instance?: "lite" | "basic" | "standard-1" | "standard-2" | "standard-3" | "standard-4" | ContainerStartResources; + labels?: Record; + directorySnapshots?: ContainerDirectorySnapshotRestoreParams[]; +} & ({ + image: string; + containerSnapshot?: never; +} | { + image?: never; + containerSnapshot?: ContainerSnapshotRestoreParams; +}); +interface ContainerInfo { + labels: Record; + image: string; +} +interface ContainerStartResources { + vcpu: number; + memoryMib: number; + diskMb: number; +} +/** + * The **`MessagePort`** interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort) + */ +declare abstract class MessagePort extends EventTarget { + /** + * The **`postMessage()`** method of the MessagePort interface sends a message from the port, and optionally, transfers ownership of objects to other browsing contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/postMessage) + */ + postMessage(data?: any, options?: (any[] | MessagePortPostMessageOptions)): void; + /** + * The **`close()`** method of the MessagePort interface disconnects the port, so it is no longer active. This stops the flow of messages to that port. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/close) + */ + close(): void; + /** + * The **`start()`** method of the MessagePort interface starts the sending of messages queued on the port. This method is only needed when using EventTarget.addEventListener; it is implied when using onmessage. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/start) + */ + start(): void; + get onmessage(): any | null; + set onmessage(value: any | null); +} +/** + * The **`MessageChannel`** interface of the Channel Messaging API allows us to create a new message channel and send data through it via its two MessagePort properties. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel) + */ +declare class MessageChannel { + constructor(); + /** + * The **`port1`** read-only property of the MessageChannel interface returns the first port of the message channel — the port attached to the context that originated the channel. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port1) + */ + readonly port1: MessagePort; + /** + * The **`port2`** read-only property of the MessageChannel interface returns the second port of the message channel — the port attached to the context at the other end of the channel, which the message is initially sent to. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port2) + */ + readonly port2: MessagePort; +} +interface MessagePortPostMessageOptions { + transfer?: any[]; +} +type LoopbackForExport Rpc.EntrypointBranded) | ExportedHandler | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? LoopbackServiceStub> : T extends new (...args: any[]) => Rpc.DurableObjectBranded ? LoopbackDurableObjectClass> : T extends new (...args: any[]) => CloudflareWorkersModule.WorkflowEntrypoint ? Workflow : T extends ExportedHandler ? LoopbackServiceStub : undefined; +type LoopbackServiceStub = Fetcher & (T extends CloudflareWorkersModule.WorkerEntrypoint ? (opts: { + props?: Props; +}) => Fetcher : (opts: { + props?: any; +}) => Fetcher); +type LoopbackDurableObjectClass = DurableObjectClass & (T extends CloudflareWorkersModule.DurableObject ? (opts: { + props?: Props; +}) => DurableObjectClass : (opts: { + props?: any; +}) => DurableObjectClass); +interface LoopbackDurableObjectNamespace extends DurableObjectNamespace { +} +interface LoopbackColoLocalActorNamespace extends ColoLocalActorNamespace { +} +interface SyncKvStorage { + get(key: string): T | undefined; + list(options?: SyncKvListOptions): Iterable<[ + string, + T + ]>; + put(key: string, value: T): void; + delete(key: string): boolean; +} +interface SyncKvListOptions { + start?: string; + startAfter?: string; + end?: string; + prefix?: string; + reverse?: boolean; + limit?: number; +} +interface WorkerStub { + getEntrypoint(name?: string, options?: WorkerStubEntrypointOptions): Fetcher; + getDurableObjectClass(name?: string, options?: WorkerStubEntrypointOptions): DurableObjectClass; +} +interface WorkerStubEntrypointOptions { + props?: any; + limits?: workerdResourceLimits; +} +interface WorkerLoader { + get(name: string | null, getCode: () => WorkerLoaderWorkerCode | Promise): WorkerStub; + load(code: WorkerLoaderWorkerCode): WorkerStub; +} +interface WorkerLoaderModule { + js?: string; + cjs?: string; + text?: string; + data?: ArrayBuffer; + json?: any; + py?: string; + wasm?: ArrayBuffer | ArrayBufferView | WebAssembly.Module; +} +interface WorkerLoaderWorkerCode { + compatibilityDate: string; + compatibilityFlags?: string[]; + allowExperimental?: boolean; + limits?: workerdResourceLimits; + mainModule: string; + modules: Record; + env?: any; + globalOutbound?: (Fetcher | null); + tails?: Fetcher[]; + streamingTails?: Fetcher[]; +} +interface workerdResourceLimits { + cpuMs?: number; + subRequests?: number; +} +/** +* The Workers runtime supports a subset of the Performance API, used to measure timing and performance, +* as well as timing of subrequests and other operations. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) +*/ +declare abstract class Performance { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */ + get timeOrigin(): number; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ + now(): number; + /** + * The **`toJSON()`** method of the Performance interface is a serializer; it returns a JSON representation of the Performance object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON) + */ + toJSON(): object; +} +interface Tracing { + enterSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; + startActiveSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; + startSpan(name: string): Span; + getActiveSpan(): Span | undefined; + Span: typeof Span; +} +declare abstract class Span { + get isTraced(): boolean; + spanContext(): TracingSpanContext; + setAttribute(key: string, value: boolean | number | string): this; + setAttributes(attributes: Record): this; + recordException(exception: string | { + code: string | number; + name?: string; + message?: string; + stack?: string; + } | { + code?: string | number; + name: string; + message?: string; + stack?: string; + } | { + code?: string | number; + name?: string; + message: string; + stack?: string; + }): void; + updateName(name: string): this; + setStatus(status: TracingSpanStatus): this; + end(): void; +} +type TracingSpanStatusCode = "unset" | "ok" | "error"; +interface TracingSpanStatus { + code: TracingSpanStatusCode; + message?: string; +} +interface TracingSpanContext { + traceId: string; + spanId: string; + traceFlags: number; +} +/** + * Represents the identity of a user authenticated via Cloudflare Access. + * This matches the result of calling /cdn-cgi/access/get-identity. + * + * The exact structure of the returned object depends on the identity provider + * configuration for the Access application. The fields below represent commonly + * available properties, but additional provider-specific fields may be present. + */ +interface CloudflareAccessIdentity extends Record { + /** The user's email address, if available from the identity provider. */ + email?: string; + /** The user's display name. */ + name?: string; + /** The user's unique identifier. */ + user_uuid?: string; + /** The Cloudflare account ID. */ + account_id?: string; + /** Login timestamp (Unix epoch seconds). */ + iat?: number; + /** The user's IP address at authentication time. */ + ip?: string; + /** Authentication methods used (e.g., "pwd"). */ + amr?: string[]; + /** Identity provider information. */ + idp?: { + id: string; + type: string; + }; + /** Geographic information about where the user authenticated. */ + geo?: { + country: string; + }; + /** Group memberships from the identity provider. */ + groups?: Array<{ + id: string; + name: string; + email?: string; + }>; + /** Device posture check results, keyed by check ID. */ + devicePosture?: Record; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} +// ============================================================================ +// Agent Memory +// +// Public type surface for user Workers binding to an Agent Memory namespace. +// ============================================================================ +/** Memory type — every memory is classified into exactly one. */ +type AgentMemoryMemoryType = "fact" | "event" | "instruction" | "task"; +/** Search intensity for recall. */ +type AgentMemoryThinkingLevel = "low" | "medium" | "high"; +/** Response verbosity for recall. */ +type AgentMemoryResponseLength = "short" | "medium" | "long"; +/** A conversation message passed to ingest(). */ +interface AgentMemoryMessage { + role: "system" | "user" | "assistant"; + content: string; + /** Optional message timestamp. */ + timestamp?: Date; +} +/** Raw memory content passed to remember(). */ +interface AgentMemoryIncomingMemory { + /** Raw memory content. The service classifies and summarizes automatically. */ + content: string; + /** Optional session identifier to associate with this memory. */ + sessionId?: string | null | undefined; +} +/** A stored memory returned from remember(), get(), and delete(). */ +interface AgentMemoryMemory { + /** Memory ID. */ + id: string; + /** Memory type. */ + type: AgentMemoryMemoryType; + /** Text summary. */ + summary: string; + /** Memory text. */ + content: string; + /** Session that created this memory. */ + sessionId: string | null; + /** Memory creation time. */ + createdAt: Date; + /** Memory last-update time. */ + updatedAt: Date; +} +/** Single entry in a list() response. Same shape as Memory minus full content. */ +type AgentMemoryMemoryListEntry = Omit; +/** A scored memory candidate in a recall result. */ +interface AgentMemoryScoredCandidate { + /** Candidate ID. */ + id: string; + /** Text summary. */ + summary: string; + /** Session that created this candidate, when known. */ + sessionId: string | null; + /** Relevance score (higher is better). Comparable only within a single query. */ + score: number; +} +/** Options for the ingest() method. */ +interface AgentMemoryIngestOptions { + /** Session identifier to associate with memories created during ingestion. */ + sessionId?: string | null | undefined; +} +/** Options for the getSummary() method. */ +interface AgentMemoryGetSummaryOptions { + /** Session identifier to retrieve session summary for. */ + sessionId?: string | null | undefined; +} +/** Response from the getSummary() method. */ +interface AgentMemoryGetSummaryResponse { + /** Markdown summary. */ + summary: string; +} +/** + * Options for the recall() method. + * + * `referenceDate` accepts a Date object, an ISO-8601 date string + * (YYYY-MM-DD), or a full ISO-8601 datetime string. When provided, this + * date is used as "today" for resolving relative time references + * ("how many days ago", "last week") instead of the server's wall-clock time. + */ +interface AgentMemoryRecallOptions { + /** Recall intensity: "low" (default), "medium", or "high". */ + thinkingLevel?: AgentMemoryThinkingLevel; + /** Response verbosity: "short", "medium" (default), or "long". */ + responseLength?: AgentMemoryResponseLength; + /** Temporal anchor for date arithmetic. */ + referenceDate?: Date | string; +} +/** Response from the recall() method. */ +interface AgentMemoryRecallResult { + /** Number of memories retrieved. */ + count: number; + /** LLM-generated answer synthesizing the matching memories. */ + answer: string; + /** Matching memories ranked by relevance. */ + candidates: AgentMemoryScoredCandidate[]; +} +/** + * Options for the list() method. + * + * `cursor` is the opaque continuation token returned by the previous page; + * pass it back unchanged to fetch the next page. `sessionId` and `type` + * are exact-match filters; combining them is allowed. + */ +interface AgentMemoryListMemoriesOptions { + /** Maximum number of memories to return. Default 20, max 500. */ + limit?: number; + /** Opaque cursor from a previous page. */ + cursor?: string; + /** Exact-match session filter. */ + sessionId?: string; + /** Exact-match memory-type filter. */ + type?: AgentMemoryMemoryType; +} +/** Response from the list() method. */ +interface AgentMemoryListMemoriesResult { + memories: AgentMemoryMemoryListEntry[]; + /** Continuation cursor; absent when this page exhausted the result set. */ + cursor?: string; +} +/** + * A single Agent Memory profile, scoped to a profile name. + * + * Returned by {@link AgentMemoryNamespace.getProfile}. + */ +declare abstract class AgentMemoryProfile { + /** + * Retrieve a memory by ID. + * + * @param memoryId - ULID of the memory to retrieve. + * @throws if the memory does not exist. + */ + get(memoryId: string): Promise; + /** + * Delete a memory by ID. + * + * Removes the memory and any source messages linked by the memory's + * source message IDs. + * + * @param memoryId - ULID of the memory to delete. + * @throws if the memory does not exist. + */ + delete(memoryId: string): Promise; + /** + * Store a memory in this profile. The content is automatically classified, + * summarized, and indexed. + * + * @param memory - Raw memory content to persist. + */ + remember(memory: AgentMemoryIncomingMemory): Promise; + /** + * Extract memories from a conversation. + * + * @param messages - Conversation messages to extract memories from. + * @param options - Optional ingest options. + */ + ingest(messages: Iterable, options?: AgentMemoryIngestOptions): Promise; + /** + * Get a profile summary. + * + * @param options - Optional getSummary options. + */ + getSummary(options?: AgentMemoryGetSummaryOptions): Promise; + /** + * Recall memories in this profile. + * + * @param query - Recall query matched against memory content and keywords. + * @param options - Optional recall parameters. + * @returns Matching memories with relevance scores and a synthesized answer. + */ + recall(query: string, options?: AgentMemoryRecallOptions): Promise; + /** + * List active memories in this profile. + * + * Returns a paginated, filterable view of stored memories. Superseded + * versions are excluded. Use the returned `cursor` (when present) to + * fetch the next page. + * + * @param options - Optional pagination and filter options. + */ + list(options?: AgentMemoryListMemoriesOptions): Promise; + /** + * Soft-delete every memory and message in this profile that is tagged + * with `sessionId`. + * + * Idempotent: deleting a sessionId that has no rows is a no-op. + * + * @param sessionId - Session to delete. + */ + deleteSession(sessionId: string): Promise; +} +/** + * Namespace-level Agent Memory binding. + * + * Used as the type of an `env.MEMORY`-style binding backed by the Agent + * Memory product. + * + * @example + * ```ts + * export default { + * async fetch(_request: Request, env: Env): Promise { + * const profile = await env.MEMORY.getProfile("wrangler-e2e"); + * const summary = await profile.getSummary(); + * return Response.json(summary); + * }, + * }; + * ``` + */ +declare abstract class AgentMemoryNamespace { + /** + * Get a memory profile by name. Profiles are isolated by namespace and + * addressed by a compound key (namespaceId:profileName). + * + * @param profileName - Profile name (validated against naming rules). + * @returns RPC target for interacting with the profile. + */ + getProfile(profileName: string): Promise; + /** + * Soft-delete a profile and schedule deferred purge. Marks all + * memories and messages as deleted. + * + * @param profileName - Name of the profile to delete. + */ + deleteProfile(profileName: string): Promise; +} +// ============ AI Search Error Interfaces ============ +interface AiSearchInternalError extends Error { +} +interface AiSearchNotFoundError extends Error { +} +// ============ AI Search Common Types ============ +/** A single message in a conversation-style search or chat request. */ +type AiSearchMessage = { + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; +}; +/** + * Common shape for `ai_search_options` used by both single-instance and multi-instance requests. + * Contains retrieval, query rewrite, reranking, and cache sub-options. + */ +type AiSearchOptions = { + retrieval?: { + /** Which retrieval backend to use. Defaults to the instance's configured index_method. */ + retrieval_type?: 'vector' | 'keyword' | 'hybrid'; + /** Fusion method for combining vector + keyword results. */ + fusion_method?: 'max' | 'rrf'; + /** How keyword terms are combined: "and" = all terms must match, "or" = any term matches. */ + keyword_match_mode?: 'and' | 'or'; + /** Minimum similarity score (0-1) for a result to be included. Default 0.4. */ + match_threshold?: number; + /** Maximum number of results to return (1-50). Default 10. */ + max_num_results?: number; + /** Vectorize metadata filters applied to the search. */ + filters?: VectorizeVectorMetadataFilter; + /** Number of surrounding chunks to include for context (0-3). Default 0. */ + context_expansion?: number; + /** If true, return only item metadata without chunk text. */ + metadata_only?: boolean; + /** If true (default), return empty results on retrieval failure instead of throwing. */ + return_on_failure?: boolean; + /** Boost results by metadata field values. Max 3 entries. */ + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + [key: string]: unknown; + }; + query_rewrite?: { + enabled?: boolean; + model?: string; + rewrite_prompt?: string; + [key: string]: unknown; + }; + reranking?: { + enabled?: boolean; + model?: string; + /** Match threshold (0-1, default 0.4) */ + match_threshold?: number; + [key: string]: unknown; + }; + cache?: { + enabled?: boolean; + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + }; + [key: string]: unknown; +}; +// ============ AI Search Request Types ============ +/** + * Request body for single-instance search. + * Exactly one of `query` or `messages` must be provided. + */ +type AiSearchSearchRequest = { + /** Simple query string. */ + query: string; + messages?: never; + ai_search_options?: AiSearchOptions; +} | { + query?: never; + /** Conversation-style input. At least one user message with non-empty content is required. */ + messages: AiSearchMessage[]; + ai_search_options?: AiSearchOptions; +}; +type AiSearchChatCompletionsRequest = { + messages: AiSearchMessage[]; + model?: string; + stream?: boolean; + ai_search_options?: AiSearchOptions; + [key: string]: unknown; +}; +// ============ AI Search Multi-Instance Types (Namespace-Scoped) ============ +/** `ai_search_options` shape for multi-instance requests — requires `instance_ids`. */ +type AiSearchMultiSearchOptions = AiSearchOptions & { + /** Instance IDs to search across (1-10). */ + instance_ids: string[]; +}; +/** + * Request for searching across multiple instances within a namespace. + * `ai_search_options` is required and must include `instance_ids`. + * Exactly one of `query` or `messages` must be provided. + */ +type AiSearchMultiSearchRequest = { + /** Simple query string. */ + query: string; + messages?: never; + ai_search_options: AiSearchMultiSearchOptions; +} | { + query?: never; + /** Conversation-style input. */ + messages: AiSearchMessage[]; + ai_search_options: AiSearchMultiSearchOptions; +}; +/** A search result chunk tagged with the instance it originated from. */ +type AiSearchMultiSearchChunk = AiSearchSearchResponse['chunks'][number] & { + instance_id: string; +}; +/** Describes a per-instance error during a multi-instance operation. */ +type AiSearchMultiSearchError = { + instance_id: string; + message: string; +}; +/** Response from a multi-instance search, with chunks tagged by instance and optional partial-failure errors. */ +type AiSearchMultiSearchResponse = { + search_query: string; + chunks: AiSearchMultiSearchChunk[]; + errors?: AiSearchMultiSearchError[]; +}; +/** Request for chat completions across multiple instances within a namespace. `ai_search_options` is required and must include `instance_ids`. */ +type AiSearchMultiChatCompletionsRequest = Omit & { + ai_search_options: AiSearchMultiSearchOptions; +}; +/** Response from multi-instance chat completions, with chunks tagged by instance and optional partial-failure errors. */ +type AiSearchMultiChatCompletionsResponse = Omit & { + chunks: AiSearchMultiSearchChunk[]; + errors?: AiSearchMultiSearchError[]; +}; +// ============ AI Search Response Types ============ +type AiSearchSearchResponse = { + search_query: string; + chunks: Array<{ + id: string; + type: string; + /** Match score (0-1) */ + score: number; + text: string; + item: { + timestamp?: number; + key: string; + metadata?: Record; + }; + scoring_details?: { + /** Keyword match score (0-1) */ + keyword_score?: number; + /** Vector similarity score (0-1) */ + vector_score?: number; + /** Keyword rank position */ + keyword_rank?: number; + /** Vector rank position */ + vector_rank?: number; + /** Reranking model score */ + reranking_score?: number; + /** Fusion method used to combine results */ + fusion_method?: 'rrf' | 'max'; + [key: string]: unknown; + }; + }>; +}; +type AiSearchChatCompletionsResponse = { + id?: string; + object?: string; + model?: string; + choices: Array<{ + index?: number; + message: { + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; + [key: string]: unknown; + }; + [key: string]: unknown; + }>; + chunks: AiSearchSearchResponse['chunks']; + [key: string]: unknown; +}; +type AiSearchStatsResponse = { + queued?: number; + running?: number; + completed?: number; + error?: number; + skipped?: number; + outdated?: number; + last_activity?: string; + /** Storage engine statistics. */ + engine?: { + vectorize?: { + vectorsCount: number; + dimensions: number; + }; + r2?: { + payloadSizeBytes: number; + metadataSizeBytes: number; + objectCount: number; + }; + }; +}; +// ============ AI Search Instance Info Types ============ +type AiSearchInstanceInfo = { + id: string; + type?: 'r2' | 'web-crawler' | string; + source?: string; + source_params?: unknown; + paused?: boolean; + status?: string; + namespace?: string; + created_at?: string; + modified_at?: string; + token_id?: string; + ai_gateway_id?: string; + rewrite_query?: boolean; + reranking?: boolean; + embedding_model?: string; + ai_search_model?: string; + rewrite_model?: string; + reranking_model?: string; + /** @deprecated Use index_method instead. */ + hybrid_search_enabled?: boolean; + /** Controls which storage backends are active. */ + index_method?: { + vector?: boolean; + keyword?: boolean; + }; + /** Fusion method for combining vector and keyword results. */ + fusion_method?: 'max' | 'rrf'; + indexing_options?: { + keyword_tokenizer?: 'porter' | 'trigram'; + } | null; + retrieval_options?: { + keyword_match_mode?: 'and' | 'or'; + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + } | null; + chunk?: boolean; + chunk_size?: number; + chunk_overlap?: number; + score_threshold?: number; + max_num_results?: number; + cache?: boolean; + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + custom_metadata?: Array<{ + field_name: string; + data_type: 'text' | 'number' | 'boolean' | 'datetime'; + }>; + /** Sync interval in seconds. */ + sync_interval?: 3600 | 7200 | 14400 | 21600 | 43200 | 86400; + metadata?: Record; + [key: string]: unknown; +}; +/** Pagination, search, and ordering parameters for listing instances within a namespace. */ +type AiSearchListInstancesParams = { + page?: number; + per_page?: number; + /** Search instances by ID. */ + search?: string; + /** Field to sort by. */ + order_by?: 'created_at'; + /** Sort direction. */ + order_by_direction?: 'asc' | 'desc'; +}; +type AiSearchListResponse = { + result: AiSearchInstanceInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Config Types ============ +type AiSearchConfig = { + /** Instance ID (1-32 chars, pattern: ^[a-z0-9_]+(?:-[a-z0-9_]+)*$) */ + id: string; + /** Instance type. Omit to create with built-in storage. */ + type?: 'r2' | 'web-crawler' | string; + /** Source URL (required for web-crawler type). */ + source?: string; + source_params?: unknown; + /** Token ID (UUID format) */ + token_id?: string; + ai_gateway_id?: string; + /** Enable query rewriting (default false) */ + rewrite_query?: boolean; + /** Enable reranking (default false) */ + reranking?: boolean; + embedding_model?: string; + ai_search_model?: string; + rewrite_model?: string; + reranking_model?: string; + /** @deprecated Use index_method instead. */ + hybrid_search_enabled?: boolean; + /** Controls which storage backends are used during indexing. Defaults to vector-only. */ + index_method?: { + vector?: boolean; + keyword?: boolean; + }; + /** Fusion method for combining vector and keyword results. "rrf" = reciprocal rank fusion (default), "max" = maximum score. */ + fusion_method?: 'max' | 'rrf'; + indexing_options?: { + keyword_tokenizer?: 'porter' | 'trigram'; + } | null; + retrieval_options?: { + keyword_match_mode?: 'and' | 'or'; + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + } | null; + chunk?: boolean; + chunk_size?: number; + chunk_overlap?: number; + /** Minimum similarity score (0-1) for a result to be included. */ + score_threshold?: number; + max_num_results?: number; + cache?: boolean; + /** Similarity threshold for cache hits. Stricter = fewer cache hits but higher relevance. */ + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + custom_metadata?: Array<{ + field_name: string; + data_type: 'text' | 'number' | 'boolean' | 'datetime'; + }>; + namespace?: string; + /** Sync interval in seconds. 3600=1h, 7200=2h, 14400=4h, 21600=6h, 43200=12h, 86400=24h. */ + sync_interval?: 3600 | 7200 | 14400 | 21600 | 43200 | 86400; + metadata?: Record; + [key: string]: unknown; +}; +// ============ AI Search Item Types ============ +type AiSearchItemInfo = { + id: string; + key: string; + status: 'completed' | 'error' | 'skipped' | 'queued' | 'running' | 'outdated'; + next_action?: 'INDEX' | 'DELETE' | null; + error?: string; + checksum?: string; + namespace?: string; + chunks_count?: number | null; + file_size?: number | null; + source_id?: string | null; + last_seen_at?: string; + created_at?: string; + metadata?: Record; + [key: string]: unknown; +}; +type AiSearchItemContentResult = { + body: ReadableStream; + contentType: string; + filename: string; + size: number; +}; +type AiSearchUploadItemOptions = { + metadata?: Record; +}; +type AiSearchListItemsParams = { + page?: number; + per_page?: number; + /** Search items by key name. */ + search?: string; + /** Sort order for results. */ + sort_by?: 'status' | 'modified_at'; + /** Filter items by processing status. */ + status?: 'queued' | 'running' | 'completed' | 'error' | 'skipped' | 'outdated'; + /** Filter items by source (e.g. "builtin" or "web-crawler:https://example.com"). */ + source?: string; + /** JSON-encoded Vectorize filter for metadata filtering. */ + metadata_filter?: string; + /** Filter items by their unique ID. Returns at most one item. */ + item_id?: string; + /** + * Filter items by their exact key (object key / filename). Keys are unique + * per source, so combine with `source` to disambiguate across data sources. + */ + key?: string; +}; +type AiSearchListItemsResponse = { + result: AiSearchItemInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Item Logs Types ============ +type AiSearchItemLogsParams = { + /** Maximum number of log entries to return (1-100, default 50). */ + limit?: number; + /** Opaque cursor for pagination. Pass the `cursor` value from a previous response. */ + cursor?: string; +}; +type AiSearchItemLog = { + timestamp: string; + action: string; + message: string; + fileKey?: string; + chunkCount?: number; + processingTimeMs?: number; + errorType?: string; +}; +/** Paginated response for item processing logs (cursor-based). */ +type AiSearchItemLogsResponse = { + result: AiSearchItemLog[]; + result_info: { + count: number; + per_page: number; + cursor: string | null; + truncated: boolean; + }; +}; +// ============ AI Search Item Chunks Types ============ +type AiSearchItemChunksParams = { + /** Maximum number of chunks to return (1-100, default 20). */ + limit?: number; + /** Offset into the chunks list (default 0). */ + offset?: number; +}; +/** A single indexed chunk belonging to an item, including its text content and byte range. */ +type AiSearchItemChunk = { + id: string; + text: string; + start_byte: number; + end_byte: number; + item?: { + timestamp?: number; + key: string; + metadata?: Record; + }; +}; +/** Paginated response for item chunks (offset-based). */ +type AiSearchItemChunksResponse = { + result: AiSearchItemChunk[]; + result_info: { + count: number; + total: number; + limit: number; + offset: number; + }; +}; +// ============ AI Search Job Types ============ +type AiSearchJobInfo = { + id: string; + source: 'user' | 'schedule'; + description?: string; + last_seen_at?: string; + started_at?: string; + ended_at?: string; + end_reason?: string; +}; +type AiSearchJobLog = { + id: number; + message: string; + message_type: number; + created_at: number; +}; +type AiSearchCreateJobParams = { + description?: string; +}; +type AiSearchListJobsParams = { + page?: number; + per_page?: number; +}; +type AiSearchListJobsResponse = { + result: AiSearchJobInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +type AiSearchJobLogsParams = { + page?: number; + per_page?: number; +}; +type AiSearchJobLogsResponse = { + result: AiSearchJobLog[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Sub-Service Classes ============ +/** + * Single item service for an AI Search instance. + * Provides info, download, sync, logs, and chunks operations on a specific item. + */ +declare abstract class AiSearchItem { + /** Get metadata about this item. */ + info(): Promise; + /** + * Download the item's content. + * @returns Object with body stream, content type, filename, and size. + */ + download(): Promise; + /** + * Trigger re-indexing of this item. + * @returns The updated item info. + */ + sync(): Promise; + /** + * Retrieve processing logs for this item (cursor-based pagination). + * @param params Optional pagination parameters (limit, cursor). + * @returns Paginated log entries for this item. + */ + logs(params?: AiSearchItemLogsParams): Promise; + /** + * List indexed chunks for this item (offset-based pagination). + * @param params Optional pagination parameters (limit, offset). + * @returns Paginated chunk entries for this item. + */ + chunks(params?: AiSearchItemChunksParams): Promise; +} +/** + * Items collection service for an AI Search instance. + * Provides list, upload, and access to individual items. + */ +declare abstract class AiSearchItems { + /** List items in this instance. */ + list(params?: AiSearchListItemsParams): Promise; + /** + * Upload a file as an item. Behaves as an upsert: if an item with the same + * filename already exists, it is overwritten and re-indexed. + * @param name Filename for the uploaded item. + * @param content File content as a ReadableStream, Blob, or string. + * @param options Optional metadata to attach to the item. + * @returns The created item info. + */ + upload(name: string, content: ReadableStream | Blob | string, options?: AiSearchUploadItemOptions): Promise; + /** + * Upload a file and poll until processing completes. + * Behaves as an upsert: if an item with the same filename already exists, + * it is overwritten and re-indexed. + * @param name Filename for the uploaded item. + * @param content File content as a ReadableStream, Blob, or string. + * @param options Optional metadata and polling configuration. + * @returns The item info after processing completes (or timeout). + */ + uploadAndPoll(name: string, content: ReadableStream | Blob | string, options?: AiSearchUploadItemOptions & { + /** Polling interval in milliseconds (default 1000). */ + pollIntervalMs?: number; + /** Maximum time to wait in milliseconds (default 30000). */ + timeoutMs?: number; + }): Promise; + /** + * Get an item by ID. + * @param itemId The item identifier. + * @returns Item service for info, download, sync, logs, and chunks operations. + */ + get(itemId: string): AiSearchItem; + /** + * Delete an item from the instance. + * @param itemId The item identifier. + */ + delete(itemId: string): Promise; +} +/** + * Single job service for an AI Search instance. + * Provides info, logs, and cancel operations for a specific job. + */ +declare abstract class AiSearchJob { + /** Get metadata about this job. */ + info(): Promise; + /** Get logs for this job. */ + logs(params?: AiSearchJobLogsParams): Promise; + /** + * Cancel a running job. + * @returns The updated job info. + * @throws AiSearchNotFoundError if the job does not exist. + */ + cancel(): Promise; +} +/** + * Jobs collection service for an AI Search instance. + * Provides list, create, and access to individual jobs. + */ +declare abstract class AiSearchJobs { + /** List jobs for this instance. */ + list(params?: AiSearchListJobsParams): Promise; + /** + * Create a new indexing job. + * @param params Optional job parameters. + * @returns The created job info. + */ + create(params?: AiSearchCreateJobParams): Promise; + /** + * Get a job by ID. + * @param jobId The job identifier. + * @returns Job service for info, logs, and cancel operations. + */ + get(jobId: string): AiSearchJob; +} +// ============ AI Search Binding Classes ============ +/** + * Instance-level AI Search service. + * + * Used as: + * - The return type of `AiSearchNamespace.get(name)` (namespace binding) + * - The type of `env.BLOG_SEARCH` (single instance binding via `ai_search`) + * + * Provides search, chat, update, stats, items, and jobs operations. + * + * @example + * ```ts + * // Via namespace binding + * const instance = env.AI_SEARCH.get("blog"); + * const results = await instance.search({ + * query: "How does caching work?", + * }); + * + * // Via single instance binding + * const results = await env.BLOG_SEARCH.search({ + * messages: [{ role: "user", content: "How does caching work?" }], + * }); + * ``` + */ +declare abstract class AiSearchInstance { + /** + * Search the AI Search instance for relevant chunks. + * @param params Search request with query or messages and optional AI search options. + * @returns Search response with matching chunks and search query. + */ + search(params: AiSearchSearchRequest): Promise; + /** + * Generate chat completions with AI Search context (streaming). + * @param params Chat completions request with stream: true. + * @returns ReadableStream of server-sent events. + */ + chatCompletions(params: AiSearchChatCompletionsRequest & { + stream: true; + }): Promise; + /** + * Generate chat completions with AI Search context. + * @param params Chat completions request. + * @returns Chat completion response with choices and RAG chunks. + */ + chatCompletions(params: AiSearchChatCompletionsRequest): Promise; + /** + * Update the instance configuration. + * @param config Partial configuration to update. + * @returns Updated instance info. + */ + update(config: Partial): Promise; + /** Get metadata about this instance. */ + info(): Promise; + /** + * Get instance statistics (item count, indexing status, etc.). + * @returns Statistics with counts per status, last activity time, and engine details. + */ + stats(): Promise; + /** Items collection — list, upload, and manage items in this instance. */ + get items(): AiSearchItems; + /** Jobs collection — list, create, and inspect indexing jobs. */ + get jobs(): AiSearchJobs; +} +/** + * Namespace-level AI Search service. + * + * Used as the type of `env.AI_SEARCH` (namespace binding via `ai_search_namespaces`). + * Scoped to a single namespace. Provides dynamic instance access, creation, deletion, + * and multi-instance search/chat operations. + * + * @example + * ```ts + * // Access an instance within the namespace + * const blog = env.AI_SEARCH.get("blog"); + * const results = await blog.search({ query: "How does caching work?" }); + * + * // List all instances in the namespace + * const instances = await env.AI_SEARCH.list(); + * + * // Create a new instance with built-in storage + * const tenant = await env.AI_SEARCH.create({ id: "tenant-123" }); + * + * // Upload items into the instance + * await tenant.items.upload("doc.pdf", fileContent); + * + * // Search across multiple instances + * const multi = await env.AI_SEARCH.search({ + * query: "caching", + * ai_search_options: { instance_ids: ["blog", "docs"] }, + * }); + * + * // Delete an instance + * await env.AI_SEARCH.delete("tenant-123"); + * ``` + */ +declare abstract class AiSearchNamespace { + /** + * Get an instance by name within the bound namespace. + * @param name Instance name. + * @returns Instance service for search, chat, update, stats, items, and jobs. + */ + get(name: string): AiSearchInstance; + /** + * List instances in the bound namespace. + * @param params Optional pagination, search, and ordering parameters. + * @returns Array of instance metadata with pagination info. + */ + list(params?: AiSearchListInstancesParams): Promise; + /** + * Create a new instance within the bound namespace. + * @param config Instance configuration. Only `id` is required — omit `type` and `source` to create with built-in storage. + * @returns Instance service for the newly created instance. + * + * @example + * ```ts + * // Create with built-in storage (upload items manually) + * const instance = await env.AI_SEARCH.create({ id: "my-search" }); + * + * // Create with web crawler source + * const instance = await env.AI_SEARCH.create({ + * id: "docs-search", + * type: "web-crawler", + * source: "https://developers.cloudflare.com", + * }); + * ``` + */ + create(config: AiSearchConfig): Promise; + /** + * Delete an instance from the bound namespace. + * @param name Instance name to delete. + */ + delete(name: string): Promise; + /** + * Search across multiple instances within the bound namespace. + * Fans out to the specified instance_ids and merges results. + * @param params Search request with required `ai_search_options.instance_ids`. + * @returns Search response with chunks tagged by instance_id and optional partial-failure errors. + */ + search(params: AiSearchMultiSearchRequest): Promise; + /** + * Generate chat completions across multiple instances within the bound namespace (streaming). + * Fans out to the specified instance_ids, merges context, and generates a response. + * @param params Chat completions request with stream: true and required `ai_search_options.instance_ids`. + * @returns ReadableStream of server-sent events. + */ + chatCompletions(params: AiSearchMultiChatCompletionsRequest & { + stream: true; + }): Promise; + /** + * Generate chat completions across multiple instances within the bound namespace. + * Fans out to the specified instance_ids, merges context, and generates a response. + * @param params Chat completions request with required `ai_search_options.instance_ids`. + * @returns Chat completion response with choices, chunks tagged by instance_id, and optional partial-failure errors. + */ + chatCompletions(params: AiSearchMultiChatCompletionsRequest): Promise; +} +type AiImageClassificationInput = { + image: number[]; +}; +type AiImageClassificationOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiImageClassification { + inputs: AiImageClassificationInput; + postProcessedOutputs: AiImageClassificationOutput; +} +type AiImageToTextInput = { + image: number[]; + prompt?: string; + max_tokens?: number; + temperature?: number; + top_p?: number; + top_k?: number; + seed?: number; + repetition_penalty?: number; + frequency_penalty?: number; + presence_penalty?: number; + raw?: boolean; + messages?: RoleScopedChatInput[]; +}; +type AiImageToTextOutput = { + description: string; +}; +declare abstract class BaseAiImageToText { + inputs: AiImageToTextInput; + postProcessedOutputs: AiImageToTextOutput; +} +type AiImageTextToTextInput = { + image: string; + prompt?: string; + max_tokens?: number; + temperature?: number; + ignore_eos?: boolean; + top_p?: number; + top_k?: number; + seed?: number; + repetition_penalty?: number; + frequency_penalty?: number; + presence_penalty?: number; + raw?: boolean; + messages?: RoleScopedChatInput[]; +}; +type AiImageTextToTextOutput = { + description: string; +}; +declare abstract class BaseAiImageTextToText { + inputs: AiImageTextToTextInput; + postProcessedOutputs: AiImageTextToTextOutput; +} +type AiMultimodalEmbeddingsInput = { + image: string; + text: string[]; +}; +type AiIMultimodalEmbeddingsOutput = { + data: number[][]; + shape: number[]; +}; +declare abstract class BaseAiMultimodalEmbeddings { + inputs: AiImageTextToTextInput; + postProcessedOutputs: AiImageTextToTextOutput; +} +type AiObjectDetectionInput = { + image: number[]; +}; +type AiObjectDetectionOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiObjectDetection { + inputs: AiObjectDetectionInput; + postProcessedOutputs: AiObjectDetectionOutput; +} +type AiSentenceSimilarityInput = { + source: string; + sentences: string[]; +}; +type AiSentenceSimilarityOutput = number[]; +declare abstract class BaseAiSentenceSimilarity { + inputs: AiSentenceSimilarityInput; + postProcessedOutputs: AiSentenceSimilarityOutput; +} +type AiAutomaticSpeechRecognitionInput = { + audio: number[]; +}; +type AiAutomaticSpeechRecognitionOutput = { + text?: string; + words?: { + word: string; + start: number; + end: number; + }[]; + vtt?: string; +}; +declare abstract class BaseAiAutomaticSpeechRecognition { + inputs: AiAutomaticSpeechRecognitionInput; + postProcessedOutputs: AiAutomaticSpeechRecognitionOutput; +} +type AiSummarizationInput = { + input_text: string; + max_length?: number; +}; +type AiSummarizationOutput = { + summary: string; +}; +declare abstract class BaseAiSummarization { + inputs: AiSummarizationInput; + postProcessedOutputs: AiSummarizationOutput; +} +type AiTextClassificationInput = { + text: string; +}; +type AiTextClassificationOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiTextClassification { + inputs: AiTextClassificationInput; + postProcessedOutputs: AiTextClassificationOutput; +} +type AiTextEmbeddingsInput = { + text: string | string[]; +}; +type AiTextEmbeddingsOutput = { + shape: number[]; + data: number[][]; +}; +declare abstract class BaseAiTextEmbeddings { + inputs: AiTextEmbeddingsInput; + postProcessedOutputs: AiTextEmbeddingsOutput; +} +type RoleScopedChatInput = { + role: "user" | "assistant" | "system" | "tool" | (string & NonNullable); + content: string; + name?: string; +}; +type AiTextGenerationToolLegacyInput = { + name: string; + description: string; + parameters?: { + type: "object" | (string & NonNullable); + properties: { + [key: string]: { + type: string; + description?: string; + }; + }; + required: string[]; + }; +}; +type AiTextGenerationToolInput = { + type: "function" | (string & NonNullable); + function: { + name: string; + description: string; + parameters?: { + type: "object" | (string & NonNullable); + properties: { + [key: string]: { + type: string; + description?: string; + }; + }; + required: string[]; + }; + }; +}; +type AiTextGenerationFunctionsInput = { + name: string; + code: string; +}; +type AiTextGenerationResponseFormat = { + type: string; + json_schema?: any; +}; +type AiTextGenerationInput = { + prompt?: string; + raw?: boolean; + stream?: boolean; + max_tokens?: number; + temperature?: number; + top_p?: number; + top_k?: number; + seed?: number; + repetition_penalty?: number; + frequency_penalty?: number; + presence_penalty?: number; + messages?: RoleScopedChatInput[]; + response_format?: AiTextGenerationResponseFormat; + tools?: AiTextGenerationToolInput[] | AiTextGenerationToolLegacyInput[] | (object & NonNullable); + functions?: AiTextGenerationFunctionsInput[]; +}; +type AiTextGenerationToolLegacyOutput = { + name: string; + arguments: unknown; +}; +type AiTextGenerationToolOutput = { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +}; +type UsageTags = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; +}; +type AiTextGenerationOutput = { + response?: string; + tool_calls?: AiTextGenerationToolLegacyOutput[] & AiTextGenerationToolOutput[]; + usage?: UsageTags; +}; +declare abstract class BaseAiTextGeneration { + inputs: AiTextGenerationInput; + postProcessedOutputs: AiTextGenerationOutput; +} +type AiTextToSpeechInput = { + prompt: string; + lang?: string; +}; +type AiTextToSpeechOutput = Uint8Array | { + audio: string; +}; +declare abstract class BaseAiTextToSpeech { + inputs: AiTextToSpeechInput; + postProcessedOutputs: AiTextToSpeechOutput; +} +type AiTextToImageInput = { + prompt: string; + negative_prompt?: string; + height?: number; + width?: number; + image?: number[]; + image_b64?: string; + mask?: number[]; + num_steps?: number; + strength?: number; + guidance?: number; + seed?: number; +}; +type AiTextToImageOutput = ReadableStream; +declare abstract class BaseAiTextToImage { + inputs: AiTextToImageInput; + postProcessedOutputs: AiTextToImageOutput; +} +type AiTranslationInput = { + text: string; + target_lang: string; + source_lang?: string; +}; +type AiTranslationOutput = { + translated_text?: string; +}; +declare abstract class BaseAiTranslation { + inputs: AiTranslationInput; + postProcessedOutputs: AiTranslationOutput; +} +/** + * Workers AI support for OpenAI's Chat Completions API + */ +type ChatCompletionContentPartText = { + type: "text"; + text: string; +}; +type ChatCompletionContentPartImage = { + type: "image_url"; + image_url: { + url: string; + detail?: "auto" | "low" | "high"; + }; +}; +type ChatCompletionContentPartInputAudio = { + type: "input_audio"; + input_audio: { + /** Base64 encoded audio data. */ + data: string; + format: "wav" | "mp3"; + }; +}; +type ChatCompletionContentPartFile = { + type: "file"; + file: { + /** Base64 encoded file data. */ + file_data?: string; + /** The ID of an uploaded file. */ + file_id?: string; + filename?: string; + }; +}; +type ChatCompletionContentPartRefusal = { + type: "refusal"; + refusal: string; +}; +type ChatCompletionContentPart = ChatCompletionContentPartText | ChatCompletionContentPartImage | ChatCompletionContentPartInputAudio | ChatCompletionContentPartFile; +type FunctionDefinition = { + name: string; + description?: string; + parameters?: Record; + strict?: boolean | null; +}; +type ChatCompletionFunctionTool = { + type: "function"; + function: FunctionDefinition; +}; +type ChatCompletionCustomToolGrammarFormat = { + type: "grammar"; + grammar: { + definition: string; + syntax: "lark" | "regex"; + }; +}; +type ChatCompletionCustomToolTextFormat = { + type: "text"; +}; +type ChatCompletionCustomToolFormat = ChatCompletionCustomToolTextFormat | ChatCompletionCustomToolGrammarFormat; +type ChatCompletionCustomTool = { + type: "custom"; + custom: { + name: string; + description?: string; + format?: ChatCompletionCustomToolFormat; + }; +}; +type ChatCompletionTool = ChatCompletionFunctionTool | ChatCompletionCustomTool; +type ChatCompletionMessageFunctionToolCall = { + id: string; + type: "function"; + function: { + name: string; + /** JSON-encoded arguments string. */ + arguments: string; + }; +}; +type ChatCompletionMessageCustomToolCall = { + id: string; + type: "custom"; + custom: { + name: string; + input: string; + }; +}; +type ChatCompletionMessageToolCall = ChatCompletionMessageFunctionToolCall | ChatCompletionMessageCustomToolCall; +type ChatCompletionToolChoiceFunction = { + type: "function"; + function: { + name: string; + }; +}; +type ChatCompletionToolChoiceCustom = { + type: "custom"; + custom: { + name: string; + }; +}; +type ChatCompletionToolChoiceAllowedTools = { + type: "allowed_tools"; + allowed_tools: { + mode: "auto" | "required"; + tools: Array>; + }; +}; +type ChatCompletionToolChoiceOption = "none" | "auto" | "required" | ChatCompletionToolChoiceFunction | ChatCompletionToolChoiceCustom | ChatCompletionToolChoiceAllowedTools; +type DeveloperMessage = { + role: "developer"; + content: string | Array<{ + type: "text"; + text: string; + }>; + name?: string; +}; +type SystemMessage = { + role: "system"; + content: string | Array<{ + type: "text"; + text: string; + }>; + name?: string; +}; +/** + * Permissive merged content part used inside UserMessage arrays. + * + * Cabidela has a limitation where anyOf/oneOf with enum-based discrimination + * inside nested array items does not correctly match different branches for + * different array elements, so the schema uses a single merged object. + */ +type UserMessageContentPart = { + type: "text" | "image_url" | "input_audio" | "file"; + text?: string; + image_url?: { + url?: string; + detail?: "auto" | "low" | "high"; + }; + input_audio?: { + data?: string; + format?: "wav" | "mp3"; + }; + file?: { + file_data?: string; + file_id?: string; + filename?: string; + }; +}; +type UserMessage = { + role: "user"; + content: string | Array; + name?: string; +}; +type AssistantMessageContentPart = { + type: "text" | "refusal"; + text?: string; + refusal?: string; +}; +type AssistantMessage = { + role: "assistant"; + content?: string | null | Array; + refusal?: string | null; + name?: string; + audio?: { + id: string; + }; + tool_calls?: Array; + function_call?: { + name: string; + arguments: string; + }; +}; +type ToolMessage = { + role: "tool"; + content: string | Array<{ + type: "text"; + text: string; + }>; + tool_call_id: string; +}; +type FunctionMessage = { + role: "function"; + content: string; + name: string; +}; +type ChatCompletionMessageParam = DeveloperMessage | SystemMessage | UserMessage | AssistantMessage | ToolMessage | FunctionMessage; +type ChatCompletionsResponseFormatText = { + type: "text"; +}; +type ChatCompletionsResponseFormatJSONObject = { + type: "json_object"; +}; +type ResponseFormatJSONSchema = { + type: "json_schema"; + json_schema: { + name: string; + description?: string; + schema?: Record; + strict?: boolean | null; + }; +}; +type ResponseFormat = ChatCompletionsResponseFormatText | ChatCompletionsResponseFormatJSONObject | ResponseFormatJSONSchema; +type ChatCompletionsStreamOptions = { + include_usage?: boolean; + include_obfuscation?: boolean; +}; +type PredictionContent = { + type: "content"; + content: string | Array<{ + type: "text"; + text: string; + }>; +}; +type AudioParams = { + voice: string | { + id: string; + }; + format: "wav" | "aac" | "mp3" | "flac" | "opus" | "pcm16"; +}; +type WebSearchUserLocation = { + type: "approximate"; + approximate: { + city?: string; + country?: string; + region?: string; + timezone?: string; + }; +}; +type WebSearchOptions = { + search_context_size?: "low" | "medium" | "high"; + user_location?: WebSearchUserLocation; +}; +// Source of truth for per-model reasoning types: each model's `reasoning_effort` metadata +// in Workers AI ConfigAPI (supported efforts, aliases, defaults, and whether reasoning can +// be turned off). The Workers AI SDK type generator (cloudflare/ai/sdk, +// apps/worker-constellation-entry/scripts/build-types) turns it into the per-model +// `inputs` types below; the developer docs model schemas come from the same metadata. +type ChatTemplateKwargs = { + /** Whether to enable reasoning. Support and defaults depend on the model. */ + enable_thinking?: boolean; + /** If false, preserves reasoning context between turns. */ + clear_thinking?: boolean; +}; +/** Shared optional properties used by both Prompt and Messages input branches. */ +type ChatCompletionsCommonOptions = { + model?: string; + audio?: AudioParams; + frequency_penalty?: number | null; + logit_bias?: Record | null; + logprobs?: boolean | null; + top_logprobs?: number | null; + max_tokens?: number | null; + max_completion_tokens?: number | null; + metadata?: Record | null; + modalities?: Array<"text" | "audio"> | null; + n?: number | null; + parallel_tool_calls?: boolean; + prediction?: PredictionContent; + presence_penalty?: number | null; + /** Reasoning effort. Supported levels depend on the model. */ + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: ChatTemplateKwargs; + response_format?: ResponseFormat; + seed?: number | null; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + stop?: string | Array | null; + store?: boolean | null; + stream?: boolean | null; + stream_options?: ChatCompletionsStreamOptions; + temperature?: number | null; + tool_choice?: ChatCompletionToolChoiceOption; + tools?: Array; + top_p?: number | null; + user?: string; + web_search_options?: WebSearchOptions; + function_call?: "none" | "auto" | { + name: string; + }; + functions?: Array; +}; +type PromptTokensDetails = { + cached_tokens?: number; + audio_tokens?: number; +}; +type CompletionTokensDetails = { + reasoning_tokens?: number; + audio_tokens?: number; + accepted_prediction_tokens?: number; + rejected_prediction_tokens?: number; +}; +type CompletionUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + prompt_tokens_details?: PromptTokensDetails; + completion_tokens_details?: CompletionTokensDetails; +}; +type ChatCompletionTopLogprob = { + token: string; + logprob: number; + bytes: Array | null; +}; +type ChatCompletionTokenLogprob = { + token: string; + logprob: number; + bytes: Array | null; + top_logprobs: Array; +}; +type ChatCompletionAudio = { + id: string; + /** Base64 encoded audio bytes. */ + data: string; + expires_at: number; + transcript: string; +}; +type ChatCompletionUrlCitation = { + type: "url_citation"; + url_citation: { + url: string; + title: string; + start_index: number; + end_index: number; + }; +}; +type ChatCompletionResponseMessage = { + role: "assistant"; + content: string | null; + refusal: string | null; + annotations?: Array; + audio?: ChatCompletionAudio; + tool_calls?: Array; + function_call?: { + name: string; + arguments: string; + } | null; +}; +type ChatCompletionLogprobs = { + content: Array | null; + refusal?: Array | null; +}; +type ChatCompletionChoice = { + index: number; + message: ChatCompletionResponseMessage; + finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | "function_call"; + logprobs: ChatCompletionLogprobs | null; +}; +type ChatCompletionsMessagesInput = { + messages: Array; +} & ChatCompletionsCommonOptions; +type ChatCompletionsOutput = { + id: string; + object: string; + created: number; + model: string; + choices: Array; + usage?: CompletionUsage; + system_fingerprint?: string | null; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; +}; +/** + * Workers AI support for OpenAI's Responses API + * Reference: https://github.com/openai/openai-node/blob/master/src/resources/responses/responses.ts + * + * It's a stripped down version from its source. + * It currently supports basic function calling, json mode and accepts images as input. + * + * It does not include types for WebSearch, CodeInterpreter, FileInputs, MCP, CustomTools. + * We plan to add those incrementally as model + platform capabilities evolve. + */ +type ResponsesInput = { + background?: boolean | null; + conversation?: string | ResponseConversationParam | null; + include?: Array | null; + input?: string | ResponseInput; + instructions?: string | null; + max_output_tokens?: number | null; + parallel_tool_calls?: boolean | null; + previous_response_id?: string | null; + prompt_cache_key?: string; + reasoning?: Reasoning | null; + safety_identifier?: string; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + stream?: boolean | null; + stream_options?: StreamOptions | null; + temperature?: number | null; + text?: ResponseTextConfig; + tool_choice?: ToolChoiceOptions | ToolChoiceFunction; + tools?: Array; + top_p?: number | null; + truncation?: "auto" | "disabled" | null; +}; +type ResponsesOutput = { + id?: string; + created_at?: number; + output_text?: string; + error?: ResponseError | null; + incomplete_details?: ResponseIncompleteDetails | null; + instructions?: string | Array | null; + object?: "response"; + output?: Array; + parallel_tool_calls?: boolean; + temperature?: number | null; + tool_choice?: ToolChoiceOptions | ToolChoiceFunction; + tools?: Array; + top_p?: number | null; + max_output_tokens?: number | null; + previous_response_id?: string | null; + prompt?: ResponsePrompt | null; + reasoning?: Reasoning | null; + safety_identifier?: string; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + status?: ResponseStatus; + text?: ResponseTextConfig; + truncation?: "auto" | "disabled" | null; + usage?: ResponseUsage; +}; +type EasyInputMessage = { + content: string | ResponseInputMessageContentList; + role: "user" | "assistant" | "system" | "developer"; + type?: "message"; +}; +type ResponsesFunctionTool = { + name: string; + parameters: { + [key: string]: unknown; + } | null; + strict: boolean | null; + type: "function"; + description?: string | null; +}; +type ResponseIncompleteDetails = { + reason?: "max_output_tokens" | "content_filter"; +}; +type ResponsePrompt = { + id: string; + variables?: { + [key: string]: string | ResponseInputText | ResponseInputImage; + } | null; + version?: string | null; +}; +type Reasoning = { + effort?: ReasoningEffort | null; + generate_summary?: "auto" | "concise" | "detailed" | null; + summary?: "auto" | "concise" | "detailed" | null; +}; +type ResponseContent = ResponseInputText | ResponseInputImage | ResponseOutputText | ResponseOutputRefusal | ResponseContentReasoningText; +type ResponseContentReasoningText = { + text: string; + type: "reasoning_text"; +}; +type ResponseConversationParam = { + id: string; +}; +type ResponseCreatedEvent = { + response: Response; + sequence_number: number; + type: "response.created"; +}; +type ResponseCustomToolCallOutput = { + call_id: string; + output: string | Array; + type: "custom_tool_call_output"; + id?: string; +}; +type ResponseError = { + code: "server_error" | "rate_limit_exceeded" | "invalid_prompt" | "vector_store_timeout" | "invalid_image" | "invalid_image_format" | "invalid_base64_image" | "invalid_image_url" | "image_too_large" | "image_too_small" | "image_parse_error" | "image_content_policy_violation" | "invalid_image_mode" | "image_file_too_large" | "unsupported_image_media_type" | "empty_image_file" | "failed_to_download_image" | "image_file_not_found"; + message: string; +}; +type ResponseErrorEvent = { + code: string | null; + message: string; + param: string | null; + sequence_number: number; + type: "error"; +}; +type ResponseFailedEvent = { + response: Response; + sequence_number: number; + type: "response.failed"; +}; +type ResponseFormatText = { + type: "text"; +}; +type ResponseFormatJSONObject = { + type: "json_object"; +}; +type ResponseFormatTextConfig = ResponseFormatText | ResponseFormatTextJSONSchemaConfig | ResponseFormatJSONObject; +type ResponseFormatTextJSONSchemaConfig = { + name: string; + schema: { + [key: string]: unknown; + }; + type: "json_schema"; + description?: string; + strict?: boolean | null; +}; +type ResponseFunctionCallArgumentsDeltaEvent = { + delta: string; + item_id: string; + output_index: number; + sequence_number: number; + type: "response.function_call_arguments.delta"; +}; +type ResponseFunctionCallArgumentsDoneEvent = { + arguments: string; + item_id: string; + name: string; + output_index: number; + sequence_number: number; + type: "response.function_call_arguments.done"; +}; +type ResponseFunctionCallOutputItem = ResponseInputTextContent | ResponseInputImageContent; +type ResponseFunctionCallOutputItemList = Array; +type ResponseFunctionToolCall = { + arguments: string; + call_id: string; + name: string; + type: "function_call"; + id?: string; + status?: "in_progress" | "completed" | "incomplete"; +}; +interface ResponseFunctionToolCallItem extends ResponseFunctionToolCall { + id: string; +} +type ResponseFunctionToolCallOutputItem = { + id: string; + call_id: string; + output: string | Array; + type: "function_call_output"; + status?: "in_progress" | "completed" | "incomplete"; +}; +type ResponseIncludable = "message.input_image.image_url" | "message.output_text.logprobs"; +type ResponseIncompleteEvent = { + response: Response; + sequence_number: number; + type: "response.incomplete"; +}; +type ResponseInput = Array; +type ResponseInputContent = ResponseInputText | ResponseInputImage; +type ResponseInputImage = { + detail: "low" | "high" | "auto"; + type: "input_image"; + /** + * Base64 encoded image + */ + image_url?: string | null; +}; +type ResponseInputImageContent = { + type: "input_image"; + detail?: "low" | "high" | "auto" | null; + /** + * Base64 encoded image + */ + image_url?: string | null; +}; +type ResponseInputItem = EasyInputMessage | ResponseInputItemMessage | ResponseOutputMessage | ResponseFunctionToolCall | ResponseInputItemFunctionCallOutput | ResponseReasoningItem; +type ResponseInputItemFunctionCallOutput = { + call_id: string; + output: string | ResponseFunctionCallOutputItemList; + type: "function_call_output"; + id?: string | null; + status?: "in_progress" | "completed" | "incomplete" | null; +}; +type ResponseInputItemMessage = { + content: ResponseInputMessageContentList; + role: "user" | "system" | "developer"; + status?: "in_progress" | "completed" | "incomplete"; + type?: "message"; +}; +type ResponseInputMessageContentList = Array; +type ResponseInputMessageItem = { + id: string; + content: ResponseInputMessageContentList; + role: "user" | "system" | "developer"; + status?: "in_progress" | "completed" | "incomplete"; + type?: "message"; +}; +type ResponseInputText = { + text: string; + type: "input_text"; +}; +type ResponseInputTextContent = { + text: string; + type: "input_text"; +}; +type ResponseItem = ResponseInputMessageItem | ResponseOutputMessage | ResponseFunctionToolCallItem | ResponseFunctionToolCallOutputItem; +type ResponseOutputItem = ResponseOutputMessage | ResponseFunctionToolCall | ResponseReasoningItem; +type ResponseOutputItemAddedEvent = { + item: ResponseOutputItem; + output_index: number; + sequence_number: number; + type: "response.output_item.added"; +}; +type ResponseOutputItemDoneEvent = { + item: ResponseOutputItem; + output_index: number; + sequence_number: number; + type: "response.output_item.done"; +}; +type ResponseOutputMessage = { + id: string; + content: Array; + role: "assistant"; + status: "in_progress" | "completed" | "incomplete"; + type: "message"; +}; +type ResponseOutputRefusal = { + refusal: string; + type: "refusal"; +}; +type ResponseOutputText = { + text: string; + type: "output_text"; + logprobs?: Array; +}; +type ResponseReasoningItem = { + id: string; + summary: Array; + type: "reasoning"; + content?: Array; + encrypted_content?: string | null; + status?: "in_progress" | "completed" | "incomplete"; +}; +type ResponseReasoningSummaryItem = { + text: string; + type: "summary_text"; +}; +type ResponseReasoningContentItem = { + text: string; + type: "reasoning_text"; +}; +type ResponseReasoningTextDeltaEvent = { + content_index: number; + delta: string; + item_id: string; + output_index: number; + sequence_number: number; + type: "response.reasoning_text.delta"; +}; +type ResponseReasoningTextDoneEvent = { + content_index: number; + item_id: string; + output_index: number; + sequence_number: number; + text: string; + type: "response.reasoning_text.done"; +}; +type ResponseRefusalDeltaEvent = { + content_index: number; + delta: string; + item_id: string; + output_index: number; + sequence_number: number; + type: "response.refusal.delta"; +}; +type ResponseRefusalDoneEvent = { + content_index: number; + item_id: string; + output_index: number; + refusal: string; + sequence_number: number; + type: "response.refusal.done"; +}; +type ResponseStatus = "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete"; +type ResponseStreamEvent = ResponseCompletedEvent | ResponseCreatedEvent | ResponseErrorEvent | ResponseFunctionCallArgumentsDeltaEvent | ResponseFunctionCallArgumentsDoneEvent | ResponseFailedEvent | ResponseIncompleteEvent | ResponseOutputItemAddedEvent | ResponseOutputItemDoneEvent | ResponseReasoningTextDeltaEvent | ResponseReasoningTextDoneEvent | ResponseRefusalDeltaEvent | ResponseRefusalDoneEvent | ResponseTextDeltaEvent | ResponseTextDoneEvent; +type ResponseCompletedEvent = { + response: Response; + sequence_number: number; + type: "response.completed"; +}; +type ResponseTextConfig = { + format?: ResponseFormatTextConfig; + verbosity?: "low" | "medium" | "high" | null; +}; +type ResponseTextDeltaEvent = { + content_index: number; + delta: string; + item_id: string; + logprobs: Array; + output_index: number; + sequence_number: number; + type: "response.output_text.delta"; +}; +type ResponseTextDoneEvent = { + content_index: number; + item_id: string; + logprobs: Array; + output_index: number; + sequence_number: number; + text: string; + type: "response.output_text.done"; +}; +type Logprob = { + token: string; + logprob: number; + top_logprobs?: Array; +}; +type TopLogprob = { + token?: string; + logprob?: number; +}; +type ResponseUsage = { + input_tokens: number; + output_tokens: number; + total_tokens: number; +}; +type Tool = ResponsesFunctionTool; +type ToolChoiceFunction = { + name: string; + type: "function"; +}; +type ToolChoiceOptions = "none"; +type ReasoningEffort = "minimal" | "low" | "medium" | "high" | null; +type StreamOptions = { + include_obfuscation?: boolean; +}; +/** Marks keys from T that aren't in U as optional never */ +type Without = { + [P in Exclude]?: never; +}; +/** Either T or U, but not both (mutually exclusive) */ +type XOR = (T & Without) | (U & Without); +type Ai_Cf_Baai_Bge_Base_En_V1_5_Input = { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; + }[]; +}; +type Ai_Cf_Baai_Bge_Base_En_V1_5_Output = { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} | Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse; +interface Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Base_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Output; +} +type Ai_Cf_Openai_Whisper_Input = string | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[]; +}; +interface Ai_Cf_Openai_Whisper_Output { + /** + * The transcription + */ + text: string; + word_count?: number; + words?: { + word?: string; + /** + * The second this word begins in the recording + */ + start?: number; + /** + * The ending second when the word completes + */ + end?: number; + }[]; + vtt?: string; +} +declare abstract class Base_Ai_Cf_Openai_Whisper { + inputs: Ai_Cf_Openai_Whisper_Input; + postProcessedOutputs: Ai_Cf_Openai_Whisper_Output; +} +type Ai_Cf_Meta_M2M100_1_2B_Input = { + /** + * The text to be translated + */ + text: string; + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string; + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + /** + * The text to be translated + */ + text: string; + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string; + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string; + }[]; +}; +type Ai_Cf_Meta_M2M100_1_2B_Output = { + /** + * The translated text in the target language + */ + translated_text?: string; +} | Ai_Cf_Meta_M2M100_1_2B_AsyncResponse; +interface Ai_Cf_Meta_M2M100_1_2B_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Meta_M2M100_1_2B { + inputs: Ai_Cf_Meta_M2M100_1_2B_Input; + postProcessedOutputs: Ai_Cf_Meta_M2M100_1_2B_Output; +} +type Ai_Cf_Baai_Bge_Small_En_V1_5_Input = { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; + }[]; +}; +type Ai_Cf_Baai_Bge_Small_En_V1_5_Output = { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} | Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse; +interface Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Small_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Output; +} +type Ai_Cf_Baai_Bge_Large_En_V1_5_Input = { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; + }[]; +}; +type Ai_Cf_Baai_Bge_Large_En_V1_5_Output = { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} | Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse; +interface Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Large_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Output; +} +type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input = string | { + /** + * The input text prompt for the model to generate a response. + */ + prompt?: string; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; + image: number[] | (string & NonNullable); + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; +}; +interface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output { + description?: string; +} +declare abstract class Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M { + inputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input; + postProcessedOutputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output; +} +type Ai_Cf_Openai_Whisper_Tiny_En_Input = string | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[]; +}; +interface Ai_Cf_Openai_Whisper_Tiny_En_Output { + /** + * The transcription + */ + text: string; + word_count?: number; + words?: { + word?: string; + /** + * The second this word begins in the recording + */ + start?: number; + /** + * The ending second when the word completes + */ + end?: number; + }[]; + vtt?: string; +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Tiny_En { + inputs: Ai_Cf_Openai_Whisper_Tiny_En_Input; + postProcessedOutputs: Ai_Cf_Openai_Whisper_Tiny_En_Output; +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input { + audio: string | { + body?: object; + contentType?: string; + }; + /** + * Supported tasks are 'translate' or 'transcribe'. + */ + task?: string; + /** + * The language of the audio being transcribed or translated. + */ + language?: string; + /** + * Preprocess the audio with a voice activity detection model. + */ + vad_filter?: boolean; + /** + * A text prompt to help provide context to the model on the contents of the audio. + */ + initial_prompt?: string; + /** + * The prefix appended to the beginning of the output of the transcription and can guide the transcription result. + */ + prefix?: string; + /** + * The number of beams to use in beam search decoding. Higher values may improve accuracy at the cost of speed. + */ + beam_size?: number; + /** + * Whether to condition on previous text during transcription. Setting to false may help prevent hallucination loops. + */ + condition_on_previous_text?: boolean; + /** + * Threshold for detecting no-speech segments. Segments with no-speech probability above this value are skipped. + */ + no_speech_threshold?: number; + /** + * Threshold for filtering out segments with high compression ratio, which often indicate repetitive or hallucinated text. + */ + compression_ratio_threshold?: number; + /** + * Threshold for filtering out segments with low average log probability, indicating low confidence. + */ + log_prob_threshold?: number; + /** + * Optional threshold (in seconds) to skip silent periods that may cause hallucinations. + */ + hallucination_silence_threshold?: number; +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output { + transcription_info?: { + /** + * The language of the audio being transcribed or translated. + */ + language?: string; + /** + * The confidence level or probability of the detected language being accurate, represented as a decimal between 0 and 1. + */ + language_probability?: number; + /** + * The total duration of the original audio file, in seconds. + */ + duration?: number; + /** + * The duration of the audio after applying Voice Activity Detection (VAD) to remove silent or irrelevant sections, in seconds. + */ + duration_after_vad?: number; + }; + /** + * The complete transcription of the audio. + */ + text: string; + /** + * The total number of words in the transcription. + */ + word_count?: number; + segments?: { + /** + * The starting time of the segment within the audio, in seconds. + */ + start?: number; + /** + * The ending time of the segment within the audio, in seconds. + */ + end?: number; + /** + * The transcription of the segment. + */ + text?: string; + /** + * The temperature used in the decoding process, controlling randomness in predictions. Lower values result in more deterministic outputs. + */ + temperature?: number; + /** + * The average log probability of the predictions for the words in this segment, indicating overall confidence. + */ + avg_logprob?: number; + /** + * The compression ratio of the input to the output, measuring how much the text was compressed during the transcription process. + */ + compression_ratio?: number; + /** + * The probability that the segment contains no speech, represented as a decimal between 0 and 1. + */ + no_speech_prob?: number; + words?: { + /** + * The individual word transcribed from the audio. + */ + word?: string; + /** + * The starting time of the word within the audio, in seconds. + */ + start?: number; + /** + * The ending time of the word within the audio, in seconds. + */ + end?: number; + }[]; + }[]; + /** + * The transcription in WebVTT format, which includes timing and text information for use in subtitles. + */ + vtt?: string; +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo { + inputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input; + postProcessedOutputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output; +} +type Ai_Cf_Baai_Bge_M3_Input = Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts | Ai_Cf_Baai_Bge_M3_Input_Embedding | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: (Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 | Ai_Cf_Baai_Bge_M3_Input_Embedding_1)[]; +}; +interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string; + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string; + }[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +interface Ai_Cf_Baai_Bge_M3_Input_Embedding { + text: string | string[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string; + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string; + }[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +interface Ai_Cf_Baai_Bge_M3_Input_Embedding_1 { + text: string | string[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +type Ai_Cf_Baai_Bge_M3_Output = Ai_Cf_Baai_Bge_M3_Output_Query | Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts | Ai_Cf_Baai_Bge_M3_Output_Embedding | Ai_Cf_Baai_Bge_M3_AsyncResponse; +interface Ai_Cf_Baai_Bge_M3_Output_Query { + response?: { + /** + * Index of the context in the request + */ + id?: number; + /** + * Score of the context under the index. + */ + score?: number; + }[]; +} +interface Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts { + response?: number[][]; + shape?: number[]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} +interface Ai_Cf_Baai_Bge_M3_Output_Embedding { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} +interface Ai_Cf_Baai_Bge_M3_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_M3 { + inputs: Ai_Cf_Baai_Bge_M3_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_M3_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * The number of diffusion steps; higher values can improve quality but take longer. + */ + steps?: number; +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output { + /** + * The generated image in Base64 format. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell { + inputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output; +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input = Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt | Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages; +interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + image?: number[] | (string & NonNullable); + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; +} +interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + image?: number[] | (string & NonNullable); + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * If true, the response will be streamed back incrementally. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = { + /** + * The generated text response from the model + */ + response?: string; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct { + inputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output; +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input = Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt | Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages | Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch; +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch { + requests?: { + /** + * User-supplied reference. This field will be present in the response as well it can be used to reference the request and response. It's NOT validated to be unique. + */ + external_reference?: string; + /** + * Prompt for the text generation model + */ + prompt?: string; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; + response_format?: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2; + }[]; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +} | string | Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse; +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast { + inputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output; +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Input { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender must alternate between 'user' and 'assistant'. + */ + role: "user" | "assistant"; + /** + * The content of the message as a string. + */ + content: string; + }[]; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Dictate the output format of the generated response. + */ + response_format?: { + /** + * Set to json_object to process and output generated text as JSON. + */ + type?: string; + }; +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Output { + response?: string | { + /** + * Whether the conversation is safe or not. + */ + safe?: boolean; + /** + * A list of what hazard categories predicted for the conversation, if the conversation is deemed unsafe. + */ + categories?: string[]; + }; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; +} +declare abstract class Base_Ai_Cf_Meta_Llama_Guard_3_8B { + inputs: Ai_Cf_Meta_Llama_Guard_3_8B_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_Guard_3_8B_Output; +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Input { + /** + * A query you wish to perform against the provided contexts. + */ + /** + * Number of returned results starting with the best score. + */ + top_k?: number; + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string; + }[]; +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Output { + response?: { + /** + * Index of the context in the request + */ + id?: number; + /** + * Score of the context under the index. + */ + score?: number; + }[]; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Reranker_Base { + inputs: Ai_Cf_Baai_Bge_Reranker_Base_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Reranker_Base_Output; +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input = Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt | Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages; +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + /** + * The content of the message as a string. + */ + content: string; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct { + inputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output; +} +type Ai_Cf_Qwen_Qwq_32B_Input = Ai_Cf_Qwen_Qwq_32B_Prompt | Ai_Cf_Qwen_Qwq_32B_Messages; +interface Ai_Cf_Qwen_Qwq_32B_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwq_32B_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Qwen_Qwq_32B_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Qwen_Qwq_32B { + inputs: Ai_Cf_Qwen_Qwq_32B_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwq_32B_Output; +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input = Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt | Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages; +interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct { + inputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input; + postProcessedOutputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output; +} +type Ai_Cf_Google_Gemma_3_12B_It_Input = Ai_Cf_Google_Gemma_3_12B_It_Prompt | Ai_Cf_Google_Gemma_3_12B_It_Messages; +interface Ai_Cf_Google_Gemma_3_12B_It_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Google_Gemma_3_12B_It_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Google_Gemma_3_12B_It_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Google_Gemma_3_12B_It { + inputs: Ai_Cf_Google_Gemma_3_12B_It_Input; + postProcessedOutputs: Ai_Cf_Google_Gemma_3_12B_It_Output; +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt | Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages | Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch; +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch { + requests: (Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner | Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner)[]; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The tool call id. + */ + id?: string; + /** + * Specifies the type of tool (e.g., 'function'). + */ + type?: string; + /** + * Details of the function tool. + */ + function?: { + /** + * The name of the tool to be called + */ + name?: string; + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + }; + }[]; +}; +declare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct { + inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output; +} +type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input = Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch; +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch { + requests: (Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1)[]; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output = Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response | string | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse; +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "chat.completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index?: number; + /** + * The message generated by the model + */ + message?: { + /** + * Role of the message author + */ + role: string; + /** + * The content of the message + */ + content: string; + /** + * Internal reasoning content (if available) + */ + reasoning_content?: string; + /** + * Tool calls made by the assistant + */ + tool_calls?: { + /** + * Unique identifier for the tool call + */ + id: string; + /** + * Type of tool call + */ + type: "function"; + function: { + /** + * Name of the function to call + */ + name: string; + /** + * JSON string of arguments for the function + */ + arguments: string; + }; + }[]; + }; + /** + * Reason why the model stopped generating + */ + finish_reason?: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "text_completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index: number; + /** + * The generated text completion + */ + text: string; + /** + * Reason why the model stopped generating + */ + finish_reason: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8 { + inputs: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output; +} +interface Ai_Cf_Deepgram_Nova_3_Input { + audio: { + body: object; + contentType: string; + }; + /** + * Sets how the model will interpret strings submitted to the custom_topic param. When strict, the model will only return topics submitted using the custom_topic param. When extended, the model will return its own detected topics in addition to those submitted using the custom_topic param. + */ + custom_topic_mode?: "extended" | "strict"; + /** + * Custom topics you want the model to detect within your input audio or text if present Submit up to 100 + */ + custom_topic?: string; + /** + * Sets how the model will interpret intents submitted to the custom_intent param. When strict, the model will only return intents submitted using the custom_intent param. When extended, the model will return its own detected intents in addition those submitted using the custom_intents param + */ + custom_intent_mode?: "extended" | "strict"; + /** + * Custom intents you want the model to detect within your input audio if present + */ + custom_intent?: string; + /** + * Identifies and extracts key entities from content in submitted audio + */ + detect_entities?: boolean; + /** + * Identifies the dominant language spoken in submitted audio + */ + detect_language?: boolean; + /** + * Recognize speaker changes. Each word in the transcript will be assigned a speaker number starting at 0 + */ + diarize?: boolean; + /** + * Identify and extract key entities from content in submitted audio + */ + dictation?: boolean; + /** + * Specify the expected encoding of your submitted audio + */ + encoding?: "linear16" | "flac" | "mulaw" | "amr-nb" | "amr-wb" | "opus" | "speex" | "g729"; + /** + * Arbitrary key-value pairs that are attached to the API response for usage in downstream processing + */ + extra?: string; + /** + * Filler Words can help transcribe interruptions in your audio, like 'uh' and 'um' + */ + filler_words?: boolean; + /** + * Key term prompting can boost or suppress specialized terminology and brands. + */ + keyterm?: string; + /** + * Keywords can boost or suppress specialized terminology and brands. + */ + keywords?: string; + /** + * The BCP-47 language tag that hints at the primary spoken language. Depending on the Model and API endpoint you choose only certain languages are available. + */ + language?: string; + /** + * Spoken measurements will be converted to their corresponding abbreviations. + */ + measurements?: boolean; + /** + * Opts out requests from the Deepgram Model Improvement Program. Refer to our Docs for pricing impacts before setting this to true. https://dpgr.am/deepgram-mip. + */ + mip_opt_out?: boolean; + /** + * Mode of operation for the model representing broad area of topic that will be talked about in the supplied audio + */ + mode?: "general" | "medical" | "finance"; + /** + * Transcribe each audio channel independently. + */ + multichannel?: boolean; + /** + * Numerals converts numbers from written format to numerical format. + */ + numerals?: boolean; + /** + * Splits audio into paragraphs to improve transcript readability. + */ + paragraphs?: boolean; + /** + * Profanity Filter looks for recognized profanity and converts it to the nearest recognized non-profane word or removes it from the transcript completely. + */ + profanity_filter?: boolean; + /** + * Add punctuation and capitalization to the transcript. + */ + punctuate?: boolean; + /** + * Redaction removes sensitive information from your transcripts. + */ + redact?: string; + /** + * Search for terms or phrases in submitted audio and replaces them. + */ + replace?: string; + /** + * Search for terms or phrases in submitted audio. + */ + search?: string; + /** + * Recognizes the sentiment throughout a transcript or text. + */ + sentiment?: boolean; + /** + * Apply formatting to transcript output. When set to true, additional formatting will be applied to transcripts to improve readability. + */ + smart_format?: boolean; + /** + * Detect topics throughout a transcript or text. + */ + topics?: boolean; + /** + * Segments speech into meaningful semantic units. + */ + utterances?: boolean; + /** + * Seconds to wait before detecting a pause between words in submitted audio. + */ + utt_split?: number; + /** + * The number of channels in the submitted audio + */ + channels?: number; + /** + * Specifies whether the streaming endpoint should provide ongoing transcription updates as more audio is received. When set to true, the endpoint sends continuous updates, meaning transcription results may evolve over time. Note: Supported only for webosockets. + */ + interim_results?: boolean; + /** + * Indicates how long model will wait to detect whether a speaker has finished speaking or pauses for a significant period of time. When set to a value, the streaming endpoint immediately finalizes the transcription for the processed time range and returns the transcript with a speech_final parameter set to true. Can also be set to false to disable endpointing + */ + endpointing?: string; + /** + * Indicates that speech has started. You'll begin receiving Speech Started messages upon speech starting. Note: Supported only for webosockets. + */ + vad_events?: boolean; + /** + * Indicates how long model will wait to send an UtteranceEnd message after a word has been transcribed. Use with interim_results. Note: Supported only for webosockets. + */ + utterance_end_ms?: boolean; +} +interface Ai_Cf_Deepgram_Nova_3_Output { + results?: { + channels?: { + alternatives?: { + confidence?: number; + transcript?: string; + words?: { + confidence?: number; + end?: number; + start?: number; + word?: string; + }[]; + }[]; + }[]; + summary?: { + result?: string; + short?: string; + }; + sentiments?: { + segments?: { + text?: string; + start_word?: number; + end_word?: number; + sentiment?: string; + sentiment_score?: number; + }[]; + average?: { + sentiment?: string; + sentiment_score?: number; + }; + }; + }; +} +declare abstract class Base_Ai_Cf_Deepgram_Nova_3 { + inputs: Ai_Cf_Deepgram_Nova_3_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Nova_3_Output; +} +interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input { + queries?: string | string[]; + /** + * Optional instruction for the task + */ + instruction?: string; + documents?: string | string[]; + text?: string | string[]; +} +interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output { + data?: number[][]; + shape?: number[]; +} +declare abstract class Base_Ai_Cf_Qwen_Qwen3_Embedding_0_6B { + inputs: Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output; +} +type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input = { + /** + * readable stream with audio data and content-type specified for that data + */ + audio: { + body: object; + contentType: string; + }; + /** + * type of data PCM data that's sent to the inference server as raw array + */ + dtype?: "uint8" | "float32" | "float64"; +} | { + /** + * base64 encoded audio data + */ + audio: string; + /** + * type of data PCM data that's sent to the inference server as raw array + */ + dtype?: "uint8" | "float32" | "float64"; +}; +interface Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output { + /** + * if true, end-of-turn was detected + */ + is_complete?: boolean; + /** + * probability of the end-of-turn detection + */ + probability?: number; +} +declare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 { + inputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input; + postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output; +} +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B { + inputs: XOR & { + reasoning?: (Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + effort?: "low" | "medium" | "high" | null; + }) | null; + }, Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }>; + postProcessedOutputs: XOR; +} +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { + inputs: XOR & { + reasoning?: (Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + effort?: "low" | "medium" | "high" | null; + }) | null; + }, Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }>; + postProcessedOutputs: XOR; +} +interface Ai_Cf_Leonardo_Phoenix_1_0_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt + */ + guidance?: number; + /** + * Random seed for reproducibility of the image generation + */ + seed?: number; + /** + * The height of the generated image in pixels + */ + height?: number; + /** + * The width of the generated image in pixels + */ + width?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + num_steps?: number; + /** + * Specify what to exclude from the generated images + */ + negative_prompt?: string; +} +/** + * The generated image in JPEG format + */ +type Ai_Cf_Leonardo_Phoenix_1_0_Output = string; +declare abstract class Base_Ai_Cf_Leonardo_Phoenix_1_0 { + inputs: Ai_Cf_Leonardo_Phoenix_1_0_Input; + postProcessedOutputs: Ai_Cf_Leonardo_Phoenix_1_0_Output; +} +interface Ai_Cf_Leonardo_Lucid_Origin_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt + */ + guidance?: number; + /** + * Random seed for reproducibility of the image generation + */ + seed?: number; + /** + * The height of the generated image in pixels + */ + height?: number; + /** + * The width of the generated image in pixels + */ + width?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + num_steps?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + steps?: number; +} +interface Ai_Cf_Leonardo_Lucid_Origin_Output { + /** + * The generated image in Base64 format. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Leonardo_Lucid_Origin { + inputs: Ai_Cf_Leonardo_Lucid_Origin_Input; + postProcessedOutputs: Ai_Cf_Leonardo_Lucid_Origin_Output; +} +interface Ai_Cf_Deepgram_Aura_1_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "angus" | "asteria" | "arcas" | "orion" | "orpheus" | "athena" | "luna" | "zeus" | "perseus" | "helios" | "hera" | "stella"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_1_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_1 { + inputs: Ai_Cf_Deepgram_Aura_1_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_1_Output; +} +interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input { + /** + * Input text to translate. Can be a single string or a list of strings. + */ + text: string | string[]; + /** + * Target langauge to translate to + */ + target_language: "asm_Beng" | "awa_Deva" | "ben_Beng" | "bho_Deva" | "brx_Deva" | "doi_Deva" | "eng_Latn" | "gom_Deva" | "gon_Deva" | "guj_Gujr" | "hin_Deva" | "hne_Deva" | "kan_Knda" | "kas_Arab" | "kas_Deva" | "kha_Latn" | "lus_Latn" | "mag_Deva" | "mai_Deva" | "mal_Mlym" | "mar_Deva" | "mni_Beng" | "mni_Mtei" | "npi_Deva" | "ory_Orya" | "pan_Guru" | "san_Deva" | "sat_Olck" | "snd_Arab" | "snd_Deva" | "tam_Taml" | "tel_Telu" | "urd_Arab" | "unr_Deva"; +} +interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output { + /** + * Translated texts + */ + translations: string[]; +} +declare abstract class Base_Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B { + inputs: Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input; + postProcessedOutputs: Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output; +} +type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input = Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch; +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch { + requests: (Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1)[]; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output = Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Response | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Response | string | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse; +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "chat.completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index?: number; + /** + * The message generated by the model + */ + message?: { + /** + * Role of the message author + */ + role: string; + /** + * The content of the message + */ + content: string; + /** + * Internal reasoning content (if available) + */ + reasoning_content?: string; + /** + * Tool calls made by the assistant + */ + tool_calls?: { + /** + * Unique identifier for the tool call + */ + id: string; + /** + * Type of tool call + */ + type: "function"; + function: { + /** + * Name of the function to call + */ + name: string; + /** + * JSON string of arguments for the function + */ + arguments: string; + }; + }[]; + }; + /** + * Reason why the model stopped generating + */ + finish_reason?: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "text_completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index: number; + /** + * The generated text completion + */ + text: string; + /** + * Reason why the model stopped generating + */ + finish_reason: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It { + inputs: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input; + postProcessedOutputs: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output; +} +interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Input { + /** + * Input text to embed. Can be a single string or a list of strings. + */ + text: string | string[]; +} +interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Output { + /** + * Embedding vectors, where each vector is a list of floats. + */ + data: number[][]; + /** + * Shape of the embedding data as [number_of_embeddings, embedding_dimension]. + * + * @minItems 2 + * @maxItems 2 + */ + shape: [ + number, + number + ]; +} +declare abstract class Base_Ai_Cf_Pfnet_Plamo_Embedding_1B { + inputs: Ai_Cf_Pfnet_Plamo_Embedding_1B_Input; + postProcessedOutputs: Ai_Cf_Pfnet_Plamo_Embedding_1B_Output; +} +interface Ai_Cf_Deepgram_Flux_Input { + /** + * Encoding of the audio stream. Currently only supports raw signed little-endian 16-bit PCM. + */ + encoding: "linear16"; + /** + * Sample rate of the audio stream in Hz. + */ + sample_rate: string; + /** + * End-of-turn confidence required to fire an eager end-of-turn event. When set, enables EagerEndOfTurn and TurnResumed events. Valid Values 0.3 - 0.9. + */ + eager_eot_threshold?: string; + /** + * End-of-turn confidence required to finish a turn. Valid Values 0.5 - 0.9. + */ + eot_threshold?: string; + /** + * A turn will be finished when this much time has passed after speech, regardless of EOT confidence. + */ + eot_timeout_ms?: string; + /** + * Keyterm prompting can improve recognition of specialized terminology. Pass multiple keyterm query parameters to boost multiple keyterms. + */ + keyterm?: string; + /** + * Opts out requests from the Deepgram Model Improvement Program. Refer to Deepgram Docs for pricing impacts before setting this to true. https://dpgr.am/deepgram-mip + */ + mip_opt_out?: "true" | "false"; + /** + * Label your requests for the purpose of identification during usage reporting + */ + tag?: string; +} +/** + * Output will be returned as websocket messages. + */ +interface Ai_Cf_Deepgram_Flux_Output { + /** + * The unique identifier of the request (uuid) + */ + request_id?: string; + /** + * Starts at 0 and increments for each message the server sends to the client. + */ + sequence_id?: number; + /** + * The type of event being reported. + */ + event?: "Update" | "StartOfTurn" | "EagerEndOfTurn" | "TurnResumed" | "EndOfTurn"; + /** + * The index of the current turn + */ + turn_index?: number; + /** + * Start time in seconds of the audio range that was transcribed + */ + audio_window_start?: number; + /** + * End time in seconds of the audio range that was transcribed + */ + audio_window_end?: number; + /** + * Text that was said over the course of the current turn + */ + transcript?: string; + /** + * The words in the transcript + */ + words?: { + /** + * The individual punctuated, properly-cased word from the transcript + */ + word: string; + /** + * Confidence that this word was transcribed correctly + */ + confidence: number; + }[]; + /** + * Confidence that no more speech is coming in this turn + */ + end_of_turn_confidence?: number; +} +declare abstract class Base_Ai_Cf_Deepgram_Flux { + inputs: Ai_Cf_Deepgram_Flux_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Flux_Output; +} +interface Ai_Cf_Deepgram_Aura_2_En_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "amalthea" | "andromeda" | "apollo" | "arcas" | "aries" | "asteria" | "athena" | "atlas" | "aurora" | "callista" | "cora" | "cordelia" | "delia" | "draco" | "electra" | "harmonia" | "helena" | "hera" | "hermes" | "hyperion" | "iris" | "janus" | "juno" | "jupiter" | "luna" | "mars" | "minerva" | "neptune" | "odysseus" | "ophelia" | "orion" | "orpheus" | "pandora" | "phoebe" | "pluto" | "saturn" | "thalia" | "theia" | "vesta" | "zeus"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_2_En_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_2_En { + inputs: Ai_Cf_Deepgram_Aura_2_En_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_2_En_Output; +} +interface Ai_Cf_Deepgram_Aura_2_Es_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "sirio" | "nestor" | "carina" | "celeste" | "alvaro" | "diana" | "aquila" | "selena" | "estrella" | "javier"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_2_Es_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_2_Es { + inputs: Ai_Cf_Deepgram_Aura_2_Es_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_2_Es_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Dev { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Output; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_4_7_Flash { + inputs: Omit & { + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. This model has no reasoning effort levels. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_5 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_6 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: high, none. Compatibility aliases (accepted by the API, not by these types): low maps to high; medium maps to high; max maps to high. + * + * @default "high" + */ + reasoning_effort?: "high" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +type Ai_Cf_Nvidia_Nemotron_3_120B_A12B_Input = Omit & { + /** + * ID of the model to use (for example, '@cf/nvidia/nemotron-3-120b-a12b'). + */ + model?: ChatCompletionsInput["model"]; + /** + * The maximum number of tokens to generate. + */ + max_tokens?: ChatCompletionsInput["max_tokens"]; + /** + * Set of key-value pairs that can be attached to the object. + */ + metadata?: ChatCompletionsInput["metadata"]; + /** + * Output types requested from the model. + */ + modalities?: ChatCompletionsInput["modalities"]; + /** + * Nemotron chat-template controls for normal reasoning, low-effort reasoning, and non-reasoning responses. + */ + chat_template_kwargs?: { + /** + * Whether to enable reasoning. Reasoning is enabled by default. + */ + enable_thinking?: boolean; + /** + * When reasoning is enabled, use Nemotron's low-effort reasoning mode, which uses significantly fewer reasoning tokens. + */ + low_effort?: boolean; + /** + * For coding agent use, Nvidia suggests setting force_nonempty_content=true + */ + force_nonempty_content?: boolean; + }; + /** + * Whether to store the output for model distillation or evaluation. + * + * @default false + */ + store?: ChatCompletionsInput["store"]; +}; +declare abstract class Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B { + inputs: Ai_Cf_Nvidia_Nemotron_3_120B_A12B_Input; + postProcessedOutputs: ChatCompletionsOutput; +} +type Ai_Cf_Google_Gemma_4_26B_A4B_It_Input = Omit & { + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. This model has no reasoning effort levels. + * + * @default true + */ + enable_thinking?: boolean; + }; +} & { + /** + * @default false + */ + skip_special_tokens?: boolean; +}; +declare abstract class Base_Ai_Cf_Google_Gemma_4_26B_A4B_It { + inputs: Ai_Cf_Google_Gemma_4_26B_A4B_It_Input; + postProcessedOutputs: ChatCompletionsOutput; +} +/** @deprecated Use Base_Ai_Cf_Google_Gemma_4_26B_A4B_It. */ +declare abstract class Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT extends Base_Ai_Cf_Google_Gemma_4_26B_A4B_It { +} +type Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Input = { + /** + * readable stream with audio data and content-type specified for that data + */ + audio: { + body: object; + contentType: string; + }; +} | { + /** + * base64 encoded audio data + */ + audio: string; + encoding?: "wav" | "flac" | "ogg" | "linear16"; + sample_rate?: number; + channels?: number; +}; +interface Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Output { + text?: string; + duration?: number; +} +declare abstract class Base_Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B { + inputs: Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Input; + postProcessedOutputs: Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Output; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_7_Code { + inputs: Omit & { + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. This model has no reasoning effort levels. + * + * @default true + */ + enable_thinking?: true; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_5_2 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, none. Compatibility aliases (accepted by the API, not by these types): low maps to high; medium maps to high; xhigh maps to max; minimal maps to none. + * + * @default "max" + */ + reasoning_effort?: "max" | "high" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +interface Ai_Cf_Moondream_Moondream3_1_9B_A2B_Input { + /** + * Which Moondream skill to run. + */ + task?: "query" | "caption" | "point" | "detect"; + /** + * Input image as a public HTTPS URL or base64 data URI. Optional for `query`; required for `caption`, `point`, and `detect`. + */ + image?: string; + /** + * Question for the `query` task. + */ + question?: string; + /** + * Caption length for the `caption` task. + */ + caption_length?: "short" | "normal" | "long"; + /** + * Object phrase to locate for `point` and `detect` tasks (e.g. 'person wearing a red shirt'). + */ + target?: string; + /** + * Enable reasoning trace for the `query` task. + */ + reasoning?: boolean; + /** + * Sampling temperature. + */ + temperature?: number; + /** + * Top-p (nucleus) sampling. + */ + top_p?: number; + /** + * Max tokens to generate for `query` and `caption`. + */ + max_tokens?: number; + /** + * Max objects to return for `point` and `detect`. + */ + max_objects?: number; + /** + * Return incremental tokens for `query` and `caption`. `point` and `detect` do not support streaming. + */ + stream?: boolean; +} +interface Ai_Cf_Moondream_Moondream3_1_9B_A2B_Output { + /** + * Reason the generation finished. + */ + finish_reason: string; + metrics: { + /** + * Number of input tokens consumed. + */ + input_tokens: number; + /** + * Number of output tokens generated. + */ + output_tokens: number; + /** + * Prefill time in milliseconds. + */ + prefill_time_ms: number; + /** + * Decode time in milliseconds. + */ + decode_time_ms: number; + /** + * Time to first token in milliseconds. + */ + ttft_ms: number; + }; + /** + * Answer text for the `query` task. Null for other tasks. + */ + answer?: string; + /** + * Caption text for the `caption` task. Null for other tasks. + */ + caption?: string; + /** + * Located points for the `point` task. Null for other tasks. + */ + points?: { + /** + * X coordinate. + */ + x: number; + /** + * Y coordinate. + */ + y: number; + }[]; + /** + * Detected bounding boxes for the `detect` task. Null for other tasks. + */ + objects?: { + /** + * Minimum X coordinate. + */ + x_min: number; + /** + * Minimum Y coordinate. + */ + y_min: number; + /** + * Maximum X coordinate. + */ + x_max: number; + /** + * Maximum Y coordinate. + */ + y_max: number; + }[]; + /** + * Reasoning trace for the `query` task when reasoning=true. Null otherwise. + */ + reasoning?: { + /** + * Reasoning text. + */ + text: string; + /** + * Grounding information. + */ + grounding?: {}[]; + }; +} +declare abstract class Base_Ai_Cf_Moondream_Moondream3_1_9B_A2B { + inputs: Ai_Cf_Moondream_Moondream3_1_9B_A2B_Input; + postProcessedOutputs: Ai_Cf_Moondream_Moondream3_1_9B_A2B_Output; +} +declare abstract class Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Flash_0731 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low, none. Compatibility aliases (accepted by the API, not by these types): minimal maps to low; medium maps to high; xhigh maps to high. + * + * @default "high" + */ + reasoning_effort?: "max" | "high" | "low" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Pro_0813 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low, none. Compatibility aliases (accepted by the API, not by these types): minimal maps to low; medium maps to high; xhigh maps to high. + * + * @default "high" + */ + reasoning_effort?: "max" | "high" | "low" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Qwen_Qwen3_8_27B { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: low, medium, xhigh. + * + * @default "xhigh" + */ + reasoning_effort?: "low" | "medium" | "xhigh" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_5_3 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low. Reasoning cannot be disabled. Compatibility aliases (accepted by the API, not by these types): none maps to max; minimal maps to max; medium maps to max; xhigh maps to max. + * + * @default "max" + */ + reasoning_effort?: "max" | "high" | "low" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_5_3_Flash { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low. Reasoning cannot be disabled. Compatibility aliases (accepted by the API, not by these types): none maps to max; minimal maps to max; medium maps to max; xhigh maps to max. + * + * @default "max" + */ + reasoning_effort?: "max" | "high" | "low" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +interface AiModels { + "@cf/huggingface/distilbert-sst-2-int8": BaseAiTextClassification; + "@cf/stabilityai/stable-diffusion-xl-base-1.0": BaseAiTextToImage; + "@cf/runwayml/stable-diffusion-v1-5-inpainting": BaseAiTextToImage; + "@cf/lykon/dreamshaper-8-lcm": BaseAiTextToImage; + "@cf/bytedance/stable-diffusion-xl-lightning": BaseAiTextToImage; + "@cf/myshell-ai/melotts": BaseAiTextToSpeech; + "@cf/google/embeddinggemma-300m": BaseAiTextEmbeddings; + "@cf/microsoft/resnet-50": BaseAiImageClassification; + "@cf/meta/llama-2-7b-chat-int8": BaseAiTextGeneration; + "@cf/mistral/mistral-7b-instruct-v0.1": BaseAiTextGeneration; + "@cf/meta/llama-2-7b-chat-fp16": BaseAiTextGeneration; + "@hf/thebloke/llama-2-13b-chat-awq": BaseAiTextGeneration; + "@hf/thebloke/mistral-7b-instruct-v0.1-awq": BaseAiTextGeneration; + "@hf/thebloke/zephyr-7b-beta-awq": BaseAiTextGeneration; + "@hf/thebloke/openhermes-2.5-mistral-7b-awq": BaseAiTextGeneration; + "@hf/thebloke/neural-chat-7b-v3-1-awq": BaseAiTextGeneration; + "@hf/thebloke/deepseek-coder-6.7b-base-awq": BaseAiTextGeneration; + "@hf/thebloke/deepseek-coder-6.7b-instruct-awq": BaseAiTextGeneration; + "@cf/deepseek-ai/deepseek-math-7b-instruct": BaseAiTextGeneration; + "@cf/defog/sqlcoder-7b-2": BaseAiTextGeneration; + "@cf/openchat/openchat-3.5-0106": BaseAiTextGeneration; + "@cf/tiiuae/falcon-7b-instruct": BaseAiTextGeneration; + "@cf/thebloke/discolm-german-7b-v1-awq": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-0.5b-chat": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-7b-chat-awq": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-14b-chat-awq": BaseAiTextGeneration; + "@cf/tinyllama/tinyllama-1.1b-chat-v1.0": BaseAiTextGeneration; + "@cf/microsoft/phi-2": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-1.8b-chat": BaseAiTextGeneration; + "@cf/mistral/mistral-7b-instruct-v0.2-lora": BaseAiTextGeneration; + "@hf/nousresearch/hermes-2-pro-mistral-7b": BaseAiTextGeneration; + "@hf/nexusflow/starling-lm-7b-beta": BaseAiTextGeneration; + "@hf/google/gemma-7b-it": BaseAiTextGeneration; + "@cf/meta-llama/llama-2-7b-chat-hf-lora": BaseAiTextGeneration; + "@cf/google/gemma-2b-it-lora": BaseAiTextGeneration; + "@cf/google/gemma-7b-it-lora": BaseAiTextGeneration; + "@hf/mistral/mistral-7b-instruct-v0.2": BaseAiTextGeneration; + "@cf/meta/llama-3-8b-instruct": BaseAiTextGeneration; + "@cf/fblgit/una-cybertron-7b-v2-bf16": BaseAiTextGeneration; + "@cf/meta/llama-3-8b-instruct-awq": BaseAiTextGeneration; + "@cf/meta/llama-3.1-8b-instruct-fp8": BaseAiTextGeneration; + "@cf/meta/llama-3.1-8b-instruct-awq": BaseAiTextGeneration; + "@cf/meta/llama-3.2-3b-instruct": BaseAiTextGeneration; + "@cf/meta/llama-3.2-1b-instruct": BaseAiTextGeneration; + "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b": BaseAiTextGeneration; + "@cf/ibm-granite/granite-4.0-h-micro": BaseAiTextGeneration; + "@cf/facebook/bart-large-cnn": BaseAiSummarization; + "@cf/llava-hf/llava-1.5-7b-hf": BaseAiImageToText; + "@cf/baai/bge-base-en-v1.5": Base_Ai_Cf_Baai_Bge_Base_En_V1_5; + "@cf/openai/whisper": Base_Ai_Cf_Openai_Whisper; + "@cf/meta/m2m100-1.2b": Base_Ai_Cf_Meta_M2M100_1_2B; + "@cf/baai/bge-small-en-v1.5": Base_Ai_Cf_Baai_Bge_Small_En_V1_5; + "@cf/baai/bge-large-en-v1.5": Base_Ai_Cf_Baai_Bge_Large_En_V1_5; + "@cf/unum/uform-gen2-qwen-500m": Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M; + "@cf/openai/whisper-tiny-en": Base_Ai_Cf_Openai_Whisper_Tiny_En; + "@cf/openai/whisper-large-v3-turbo": Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo; + "@cf/baai/bge-m3": Base_Ai_Cf_Baai_Bge_M3; + "@cf/black-forest-labs/flux-1-schnell": Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell; + "@cf/meta/llama-3.2-11b-vision-instruct": Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct; + "@cf/meta/llama-3.3-70b-instruct-fp8-fast": Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast; + "@cf/meta/llama-guard-3-8b": Base_Ai_Cf_Meta_Llama_Guard_3_8B; + "@cf/baai/bge-reranker-base": Base_Ai_Cf_Baai_Bge_Reranker_Base; + "@cf/qwen/qwen2.5-coder-32b-instruct": Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct; + "@cf/qwen/qwq-32b": Base_Ai_Cf_Qwen_Qwq_32B; + "@cf/mistralai/mistral-small-3.1-24b-instruct": Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct; + "@cf/google/gemma-3-12b-it": Base_Ai_Cf_Google_Gemma_3_12B_It; + "@cf/meta/llama-4-scout-17b-16e-instruct": Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct; + "@cf/qwen/qwen3-30b-a3b-fp8": Base_Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8; + "@cf/deepgram/nova-3": Base_Ai_Cf_Deepgram_Nova_3; + "@cf/qwen/qwen3-embedding-0.6b": Base_Ai_Cf_Qwen_Qwen3_Embedding_0_6B; + "@cf/pipecat-ai/smart-turn-v2": Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2; + "@cf/openai/gpt-oss-120b": Base_Ai_Cf_Openai_Gpt_Oss_120B; + "@cf/openai/gpt-oss-20b": Base_Ai_Cf_Openai_Gpt_Oss_20B; + "@cf/leonardo/phoenix-1.0": Base_Ai_Cf_Leonardo_Phoenix_1_0; + "@cf/leonardo/lucid-origin": Base_Ai_Cf_Leonardo_Lucid_Origin; + "@cf/deepgram/aura-1": Base_Ai_Cf_Deepgram_Aura_1; + "@cf/ai4bharat/indictrans2-en-indic-1B": Base_Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B; + "@cf/aisingapore/gemma-sea-lion-v4-27b-it": Base_Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It; + "@cf/pfnet/plamo-embedding-1b": Base_Ai_Cf_Pfnet_Plamo_Embedding_1B; + "@cf/deepgram/flux": Base_Ai_Cf_Deepgram_Flux; + "@cf/deepgram/aura-2-en": Base_Ai_Cf_Deepgram_Aura_2_En; + "@cf/deepgram/aura-2-es": Base_Ai_Cf_Deepgram_Aura_2_Es; + "@cf/black-forest-labs/flux-2-dev": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Dev; + "@cf/black-forest-labs/flux-2-klein-4b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B; + "@cf/black-forest-labs/flux-2-klein-9b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B; + "@cf/zai-org/glm-4.7-flash": Base_Ai_Cf_Zai_Org_Glm_4_7_Flash; + "@cf/moonshotai/kimi-k2.5": Base_Ai_Cf_Moonshotai_Kimi_K2_5; + "@cf/moonshotai/kimi-k2.6": Base_Ai_Cf_Moonshotai_Kimi_K2_6; + "@cf/nvidia/nemotron-3-120b-a12b": Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B; + "@cf/google/gemma-4-26b-a4b-it": Base_Ai_Cf_Google_Gemma_4_26B_A4B_It; + "@cf/nvidia/nemotron-speech-streaming-en-0.6b": Base_Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B; + "@cf/moonshotai/kimi-k2.7-code": Base_Ai_Cf_Moonshotai_Kimi_K2_7_Code; + "@cf/zai-org/glm-5.2": Base_Ai_Cf_Zai_Org_Glm_5_2; + "@cf/moondream/moondream3.1-9B-A2B": Base_Ai_Cf_Moondream_Moondream3_1_9B_A2B; + "@cf/deepseek-ai/deepseek-v4-flash-0731": Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Flash_0731; + "@cf/deepseek-ai/deepseek-v4-pro-0813": Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Pro_0813; + "@cf/qwen/qwen3.8-27b": Base_Ai_Cf_Qwen_Qwen3_8_27B; + "@cf/zai-org/glm-5.3": Base_Ai_Cf_Zai_Org_Glm_5_3; + "@cf/zai-org/glm-5.3-flash": Base_Ai_Cf_Zai_Org_Glm_5_3_Flash; +} +type AiOptions = { + /** + * Send requests as an asynchronous batch job, only works for supported models + * https://developers.cloudflare.com/workers-ai/features/batch-api + */ + queueRequest?: boolean; + /** + * Establish websocket connections, only works for supported models + */ + websocket?: boolean; + /** + * Tag your requests to group and view them in Cloudflare dashboard. + * + * Rules: + * Tags must only contain letters, numbers, and the symbols: : - . / @ + * Each tag can have maximum 50 characters. + * Maximum 5 tags are allowed each request. + * Duplicate tags will removed. + */ + tags?: string[]; + gateway?: GatewayOptions; + returnRawResponse?: boolean; + prefix?: string; + extraHeaders?: object; + signal?: AbortSignal; +}; +type AiModelsSearchParams = { + author?: string; + hide_experimental?: boolean; + page?: number; + per_page?: number; + search?: string; + source?: number; + task?: string; +}; +type AiModelsSearchObject = { + id: string; + source: number; + name: string; + description: string; + task: { + id: string; + name: string; + description: string; + }; + tags: string[]; + properties: { + property_id: string; + value: string; + }[]; +}; +type ChatCompletionsBase = ChatCompletionsMessagesInput; +type ChatCompletionsInput = ChatCompletionsMessagesInput; +interface InferenceUpstreamError extends Error { +} +interface AiInternalError extends Error { +} +type AiModelListType = Record; +type AiAsyncBatchResponse = { + request_id: string; +}; +type AiWebSearchRequest = { + /** AI Gateway configuration used for this request. */ + gatewayId: string; + /** Search query. */ + query: string; + /** Maximum number of results. Defaults to 10 and is capped at 20. */ + limit?: number; + /** Optional BYOK web-search provider configured on the gateway. */ + provider?: string; + /** Optional BYOK key alias. Defaults to `default`. */ + byokAlias?: string; +}; +declare abstract class Ai { + aiGatewayLogId: string | null; + gateway(gatewayId: string): AiGateway; + websearch(request: AiWebSearchRequest): Promise; + /** + * @deprecated Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(): AiSearchNamespace; + /** + * @deprecated AutoRAG has been replaced by AI Search. + * Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + * + * @param autoragId Instance ID + */ + autorag(autoragId: string): AutoRAG; + // Batch request + run(model: Name, inputs: { + requests: AiModelList[Name]['inputs'][]; + }, options: AiOptions & { + queueRequest: true; + }): Promise; + // Raw response + run(model: Name, inputs: AiModelList[Name]['inputs'], options: AiOptions & { + returnRawResponse: true; + }): Promise; + // WebSocket + run(model: Name, inputs: AiModelList[Name]['inputs'], options: AiOptions & { + websocket: true; + }): Promise; + // Streaming + run(model: Name, inputs: AiModelList[Name]['inputs'] & { + stream: true; + }, options?: AiOptions): Promise; + // Normal (default) - known model + run(model: Name, inputs: AiModelList[Name]['inputs'], options?: AiOptions): Promise; + // Unknown model (fallback). + // + // The `Exclude<..., keyof AiModelList>` constraint forces TypeScript to + // route any model name that is a literal key of `AiModelList` to one of + // the known-model overloads above (so input/output mismatches surface as + // type errors rather than silently falling back to `Record`). + // Names that aren't in `AiModelList` — e.g. third-party gateway models + // like `"google/nano-banana"` — still hit this overload. + run(model: Model extends keyof AiModelList ? never : Model, inputs: Record, options?: AiOptions): Promise>; + models(params?: AiModelsSearchParams): Promise; + toMarkdown(): ToMarkdownService; + toMarkdown(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise; + toMarkdown(files: MarkdownDocument, options?: ConversionRequestOptions): Promise; +} +type GatewayRetries = { + maxAttempts?: 1 | 2 | 3 | 4 | 5; + retryDelayMs?: number; + backoff?: 'constant' | 'linear' | 'exponential'; +}; +type GatewayOptions = { + id: string; + cacheKey?: string; + cacheTtl?: number; + skipCache?: boolean; + metadata?: Record; + collectLog?: boolean; + eventId?: string; + requestTimeoutMs?: number; + retries?: GatewayRetries; +}; +type UniversalGatewayOptions = Exclude & { + /** + ** @deprecated + */ + id?: string; +}; +type AiGatewayPatchLog = { + score?: number | null; + feedback?: -1 | 1 | null; + metadata?: Record | null; +}; +type AiGatewayLog = { + id: string; + provider: string; + model: string; + model_type?: string; + path: string; + duration: number; + request_type?: string; + request_content_type?: string; + status_code: number; + response_content_type?: string; + success: boolean; + cached: boolean; + tokens_in?: number; + tokens_out?: number; + metadata?: Record; + step?: number; + cost?: number; + custom_cost?: boolean; + request_size: number; + request_head?: string; + request_head_complete: boolean; + response_size: number; + response_head?: string; + response_head_complete: boolean; + created_at: Date; +}; +type AIGatewayProviders = 'workers-ai' | 'anthropic' | 'aws-bedrock' | 'azure-openai' | 'google-vertex-ai' | 'huggingface' | 'openai' | 'perplexity-ai' | 'replicate' | 'groq' | 'cohere' | 'google-ai-studio' | 'mistral' | 'grok' | 'openrouter' | 'deepseek' | 'cerebras' | 'cartesia' | 'elevenlabs' | 'adobe-firefly'; +type AIGatewayHeaders = { + 'cf-aig-metadata': Record | string; + 'cf-aig-custom-cost': { + per_token_in?: number; + per_token_out?: number; + } | { + total_cost?: number; + } | string; + 'cf-aig-cache-ttl': number | string; + 'cf-aig-skip-cache': boolean | string; + 'cf-aig-cache-key': string; + 'cf-aig-event-id': string; + 'cf-aig-request-timeout': number | string; + 'cf-aig-max-attempts': number | string; + 'cf-aig-retry-delay': number | string; + 'cf-aig-backoff': string; + 'cf-aig-collect-log': boolean | string; + Authorization: string; + 'Content-Type': string; + [key: string]: string | number | boolean | object; +}; +type AIGatewayUniversalRequest = { + provider: AIGatewayProviders | string; // eslint-disable-line + endpoint: string; + headers: Partial; + query: unknown; +}; +interface AiGatewayInternalError extends Error { +} +interface AiGatewayLogNotFound extends Error { +} +declare abstract class AiGateway { + patchLog(logId: string, data: AiGatewayPatchLog): Promise; + getLog(logId: string): Promise; + run(data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], options?: { + gateway?: UniversalGatewayOptions; + extraHeaders?: object; + signal?: AbortSignal; + }): Promise; + getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line +} +/** A parameter accepted by an Analytics SQL query. */ +type AnalyticsSQLParameter = string | number | boolean | null; +/** An Analytics SQL query and its optional positional or named parameters. */ +interface AnalyticsSQLQuery { + query: string; + params?: readonly AnalyticsSQLParameter[] | Readonly>; +} +/** Execution statistics returned by Analytics SQL. */ +interface AnalyticsSQLStatistics { + elapsed_ms: number; + rows_read: number; + bytes_read: number; +} +/** The rows and execution statistics returned by an Analytics SQL query. */ +interface AnalyticsSQLResult = Record> { + data: T[]; + rows: number; + statistics: AnalyticsSQLStatistics; +} +/** An Analytics SQL binding. */ +interface AnalyticsSQLBinding { + query = Record>(request: AnalyticsSQLQuery): Promise>; +} +// Copyright (c) 2022-2025 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +/** + * Artifacts — Git-compatible file storage on Cloudflare Workers. + * + * Provides programmatic access to create, manage, and fork repositories, + * and to issue and revoke scoped access tokens. + */ +/** Information about a repository. */ +interface ArtifactsRepoInfo { + /** Unique repository ID. */ + id: string; + /** Repository name. */ + name: string; + /** Repository description, or null if not set. */ + description: string | null; + /** Default branch name (e.g. "main"). */ + defaultBranch: string; + /** ISO 8601 creation timestamp. */ + createdAt: string; + /** ISO 8601 last-updated timestamp. */ + updatedAt: string; + /** ISO 8601 timestamp of the last push, or null if never pushed. */ + lastPushAt: string | null; + /** Fork source (e.g. "github:owner/repo", "artifacts:namespace/repo"), or null if not a fork. */ + source: string | null; + /** Whether the repository is read-only. */ + readOnly: boolean; + /** HTTPS git remote URL. */ + remote: string; +} +/** Result of creating a repository — includes the initial access token. */ +interface ArtifactsCreateRepoResult { + /** Unique repository ID. */ + id: string; + /** Repository name. */ + name: string; + /** Repository description, or null if not set. */ + description: string | null; + /** Default branch name. */ + defaultBranch: string; + /** HTTPS git remote URL. */ + remote: string; + /** Plaintext access token (only returned at creation time). */ + token: string; +} +/** Paginated list of repositories. */ +interface ArtifactsRepoListResult { + /** Repositories in this page (without the `remote` field). */ + repos: Omit[]; + /** Total number of repositories in the namespace. */ + total: number; + /** Cursor for the next page, if there are more results. */ + cursor?: string; +} +/** Result of creating an access token. */ +interface ArtifactsCreateTokenResult { + /** Unique token ID. */ + id: string; + /** Plaintext token (only returned at creation time). */ + plaintext: string; + /** Token scope: "read" or "write". */ + scope: 'read' | 'write'; + /** ISO 8601 token expiry timestamp. */ + expiresAt: string; +} +/** Token metadata (no plaintext). */ +interface ArtifactsTokenInfo { + /** Unique token ID. */ + id: string; + /** Token scope: "read" or "write". */ + scope: 'read' | 'write'; + /** Token state: "active", "expired", or "revoked". */ + state: 'active' | 'expired' | 'revoked'; + /** ISO 8601 creation timestamp. */ + createdAt: string; + /** ISO 8601 expiry timestamp. */ + expiresAt: string; +} +/** Paginated list of tokens for a repository. */ +interface ArtifactsTokenListResult { + /** Tokens in this page. */ + tokens: ArtifactsTokenInfo[]; + /** Total number of tokens for the repository. */ + total: number; +} +/** + * Classification of a Git tree entry derived from its mode. + * + * `tree` is a directory, `blob` is a regular file, `symlink` is a symbolic link, + * `gitlink` is a submodule reference, and `exec` is an executable file. + */ +type ArtifactsTreeEntryType = 'tree' | 'blob' | 'symlink' | 'gitlink' | 'exec'; +/** An immediate child of a Git tree returned by {@link ArtifactsRepo.readTree}. */ +interface ArtifactsTreeEntry { + /** Name relative to the tree being read. */ + name: string; + /** Canonical Git mode, such as `100644` for a file or `40000` for a tree. */ + mode: string; + /** Lowercase, 40-character SHA-1 object ID. */ + hash: string; + /** Classification derived from `mode`. */ + type: ArtifactsTreeEntryType; +} +/** Decoded metadata returned by {@link ArtifactsRepo.readCommit} and {@link ArtifactsRepo.log}. */ +interface ArtifactsCommitMetadata { + /** Lowercase, 40-character SHA-1 commit ID. */ + hash: string; + /** Lowercase, 40-character SHA-1 ID of the commit's root tree. */ + treeHash: string; + /** Commit message with one trailing newline removed, if present. */ + message: string; + /** Author identity from the commit. */ + author: { + /** Author name. */ + name: string; + /** Author email address. */ + email: string; + }; + /** Committer identity from the commit. */ + committer: { + /** Committer name. */ + name: string; + /** Committer email address. */ + email: string; + }; + /** Parent commit IDs in Git order; empty for a root commit. */ + parents: string[]; + /** Author timestamp in Unix seconds. */ + authoredAt: number; + /** Committer timestamp in Unix seconds. */ + committedAt: number; +} +/** + * Repository capability returned by {@link Artifacts.get}. + * + * Metadata is available through {@link info}, not as properties on the capability. + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + * + * The capability is an RPC stub. Dispose of it when finished, for example with + * `using repo = await env.ARTIFACTS.get(name);`, to release it before the request ends. + */ +interface ArtifactsRepo extends Disposable { + /** + * Create an access token for this repo. + * @param scope Token scope: "write" (default) or "read". + * @param ttl Time-to-live in seconds (default 86400, min 60, max 31536000). + * @throws {ArtifactsError} with code `INVALID_TTL` if ttl is out of range. + */ + createToken(scope?: 'write' | 'read', ttl?: number): Promise; + /** List tokens for this repo (metadata only, no plaintext). */ + listTokens(): Promise; + /** + * Revoke a token by plaintext or ID. + * @param tokenOrId Plaintext token or token ID. + * @returns true if revoked, false if not found. + * @throws {ArtifactsError} with code `INVALID_INPUT` if tokenOrId is empty. + */ + revokeToken(tokenOrId: string): Promise; + /** + * Retrieve current repository metadata. Each call performs a fresh lookup. + * @returns Current public repository metadata. + * @throws {ArtifactsError} `NOT_FOUND` if the repository was deleted, or `INTERNAL_ERROR` on + * an unexpected lookup failure. + */ + info(): Promise; + /** + * Read the raw bytes of a Git blob by object ID. + * @param hash Lowercase, 40-character SHA-1 object ID. + * @returns An untyped {@link Blob}, or `null` if the object is missing or is not a Git blob. + * @throws {ArtifactsError} `INVALID_INPUT` for a malformed hash, `MEMORY_LIMIT` if the object + * cannot be buffered safely, or `INTERNAL_ERROR` on an unexpected read failure. + * @see {@link ArtifactsRepo.readFile} to resolve a file by ref and path. + */ + readBlob(hash: string): Promise; + /** + * Read the immediate children of a Git tree by object ID. + * @param hash Lowercase, 40-character SHA-1 tree ID. + * @returns Tree entries, or `null` if the object is missing. + * @throws {ArtifactsError} `INVALID_INPUT` for a malformed hash, or `INTERNAL_ERROR` if the + * object is not a valid tree or the read fails unexpectedly. + * @see {@link ArtifactsTreeEntry} + */ + readTree(hash: string): Promise; + /** + * Decode a Git commit by object ID. + * @param hash Lowercase, 40-character SHA-1 commit ID. + * @returns Commit metadata, or `null` if the object is missing. + * @throws {ArtifactsError} `INVALID_INPUT` for a malformed hash, or `INTERNAL_ERROR` if the + * object is not a valid commit or the read fails unexpectedly. + * @see {@link ArtifactsCommitMetadata} + */ + readCommit(hash: string): Promise; + /** + * Resolve a file from a branch, tag, or commit ID and return its bytes with a browser-safe + * content type in {@link Blob.type}. + * @param args File lookup options. + * @param args.ref Branch, tag, or commit ID to resolve. + * @param args.path Non-empty repository-relative path. + * @returns A MIME-typed {@link Blob}, or `null` if the ref or path cannot resolve to a file. + * @throws {ArtifactsError} `INVALID_INPUT` if either argument is empty, `MEMORY_LIMIT` if the + * file cannot be buffered safely, or `INTERNAL_ERROR` on an unexpected read failure. + */ + readFile(args: { + ref: string; + path: string; + }): Promise; + /** + * List commits along the first-parent chain, newest first. + * @param opts History options. All fields are optional. + * @param opts.ref Branch, tag, or commit ID; defaults to `HEAD`. + * @param opts.limit Maximum results; defaults to `50` and is capped at `1000`. + * @param opts.offset Number of matching commits to skip; defaults to `0`. + * @returns Commit metadata, or an empty array if the ref cannot be resolved. + * @throws {ArtifactsError} `INTERNAL_ERROR` on an unexpected traversal failure. + * @see {@link ArtifactsCommitMetadata} + */ + log(opts?: { + ref?: string; + limit?: number; + offset?: number; + }): Promise; + // ── Fork ── + /** + * Fork this repo to a new repo. + * @param name Target repository name. + * @param opts Optional: description, readOnly flag, defaultBranchOnly (default true). + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if a fork is already running. + */ + fork(name: string, opts?: { + description?: string; + readOnly?: boolean; + defaultBranchOnly?: boolean; + }): Promise; +} +// ── Error types ────────────────────────────────────────────────────────────── +/** + * Error codes returned by Artifacts binding operations. + * + * Each code maps to a numeric code available on `ArtifactsError.numericCode`. + */ +type ArtifactsErrorCode = 'ALREADY_EXISTS' | 'NOT_FOUND' | 'CREATE_IN_PROGRESS' | 'IMPORT_IN_PROGRESS' | 'FORK_IN_PROGRESS' | 'INVALID_INPUT' | 'INVALID_REPO_NAME' | 'INVALID_TTL' | 'INVALID_URL' | 'REMOTE_AUTH_REQUIRED' | 'UPSTREAM_UNAVAILABLE' | 'MEMORY_LIMIT' | 'INTERNAL_ERROR'; +/** + * Error thrown by Artifacts binding operations. + * + * Uses a string `.code` discriminator following the Cloudflare platform + * convention (StreamError, ImagesError, etc.). The `.numericCode` matches + * the REST API `errors[].code` values. + */ +interface ArtifactsError extends Error { + readonly name: 'ArtifactsError'; + /** String error code for programmatic matching. */ + readonly code: ArtifactsErrorCode; + /** Numeric error code matching the REST API. */ + readonly numericCode: number; +} +// ── Binding ────────────────────────────────────────────────────────────────── +/** + * Artifacts binding — namespace-level operations. + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ +interface Artifacts { + /** + * Create a new repository with an initial access token. + * @param name Repository name (alphanumeric, dots, hyphens, underscores). + * @param opts Optional: readOnly flag, description, default branch name. + * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the repo already exists. + */ + create(name: string, opts?: { + readOnly?: boolean; + description?: string; + setDefaultBranch?: string; + }): Promise; + /** + * Get a handle to an existing repository. + * @param name Repository name. + * @returns Repo handle. + * @throws {ArtifactsError} with code `NOT_FOUND` if the repo does not exist. + * @throws {ArtifactsError} with code `CREATE_IN_PROGRESS` if the repo is still being created. + * @throws {ArtifactsError} with code `IMPORT_IN_PROGRESS` if the repo is still importing. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if the repo is still forking. + */ + get(name: string): Promise; + /** + * Import a repository from an external git remote. + * @param params Source URL and optional branch/depth, plus target name and options. + * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if the target name is invalid. + * @throws {ArtifactsError} with code `INVALID_INPUT` if the source URL is not valid HTTPS. + * @throws {ArtifactsError} with code `INVALID_URL` if the source URL does not point to a git repository. + * @throws {ArtifactsError} with code `REMOTE_AUTH_REQUIRED` if the remote requires authentication. + * @throws {ArtifactsError} with code `NOT_FOUND` if the remote repository does not exist. + * @throws {ArtifactsError} with code `UPSTREAM_UNAVAILABLE` if the remote cannot be reached. + * @throws {ArtifactsError} with code `MEMORY_LIMIT` if the import exceeds service memory limits. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + */ + import(params: { + source: { + url: string; + branch?: string; + depth?: number; + }; + target: { + name: string; + opts?: { + description?: string; + readOnly?: boolean; + }; + }; + }): Promise; + /** + * List repositories with cursor-based pagination. + * @param opts Optional: limit (1–200, default 50), cursor for next page. + */ + list(opts?: { + limit?: number; + cursor?: string; + }): Promise; + /** + * Delete a repository and all associated tokens. + * @param name Repository name. + * @returns true if deleted, false if not found. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + */ + delete(name: string): Promise; +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGInternalError extends Error { +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGNotFoundError extends Error { +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGUnauthorizedError extends Error { +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGNameNotSetError extends Error { +} +type ComparisonFilter = { + key: string; + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; + value: string | number | boolean; +}; +type CompoundFilter = { + type: 'and' | 'or'; + filters: ComparisonFilter[]; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagSearchRequest = { + query: string; + filters?: CompoundFilter | ComparisonFilter; + max_num_results?: number; + ranking_options?: { + ranker?: string; + score_threshold?: number; + }; + reranking?: { + enabled?: boolean; + model?: string; + }; + rewrite_query?: boolean; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagAiSearchRequest = AutoRagSearchRequest & { + stream?: boolean; + system_prompt?: string; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagAiSearchRequestStreaming = Omit & { + stream: true; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagSearchResponse = { + object: 'vector_store.search_results.page'; + search_query: string; + data: { + file_id: string; + filename: string; + score: number; + attributes: Record; + content: { + type: 'text'; + text: string; + }[]; + }[]; + has_more: boolean; + next_page: string | null; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagListResponse = { + id: string; + enable: boolean; + type: string; + source: string; + vectorize_name: string; + paused: boolean; + status: string; +}[]; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagAiSearchResponse = AutoRagSearchResponse & { + response: string; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +declare abstract class AutoRAG { + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + list(): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + search(params: AutoRagSearchRequest): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(params: AutoRagAiSearchRequestStreaming): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(params: AutoRagAiSearchRequest): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(params: AutoRagAiSearchRequest): Promise; +} +type BrowserRunLifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; +type BrowserRunResourceType = 'document' | 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'texttrack' | 'xhr' | 'fetch' | 'prefetch' | 'eventsource' | 'websocket' | 'manifest' | 'signedexchange' | 'ping' | 'cspviolationreport' | 'preflight' | 'other'; +/** Options fields shared by all quick actions. */ +interface BrowserRunBaseOptions { + /** Adds `