Repository navigation
docs: rebuild convt.app/docs on Blume with a full API reference - #61
Conversation
| while (next < files.length) { | ||
| const file = files[next++]; | ||
| try { | ||
| const saved = await convertFile(file, to, { pollMs }); | ||
| console.log(`${file} -> ${saved.join(", ")}`); |
There was a problem hiding this comment.
🔴 Fast batches exhaust the API limit
When conversions finish quickly, pollMs does not pace convertFile calls; workers immediately create, start, and download more jobs. A batch exceeding 40 files per minute hits the 120-request limit, and short retries fail before reset.
Learn more
The API counts create, start, status, and download requests against a fixed 120-per-minute key limit; signed uploads and output downloads do not count. The loop starts the next conversion as soon as the prior one finishes, while pollMs only controls status checks inside convert. Forty-one short conversions need at least 123 create/start/download requests in the same minute, before any polls; the helper's five short 429 attempts cannot reliably wait for a minute-long window to reset.
Example: Four workers convert 45 small PNGs to WebP in 30 seconds. The 135 required API calls alone exceed the key's 120-request window; some conversions report failures although every input is valid.
Recommended fix: Add a shared per-key request scheduler covering create, start, status, cancel, and download calls, or wait for the window to reset after 429. Account for bursts as well as average polling rate; a concurrency limit alone does not cap requests per minute.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const res = await fetch(output.url); | ||
| const name = output.name.replace(/^input/, "photo"); | ||
| await writeFile(name, Buffer.from(await res.arrayBuffer())); | ||
| console.log(`saved ${name}`); |
There was a problem hiding this comment.
🔴 Failed downloads overwrite saved conversions
When a signed URL expires or storage rejects it, the quick-start fetch writes the error body without checking res.ok. writeFile can replace an existing valid output, then report the corrupt file as saved.
| const res = await fetch(output.url); | |
| const name = output.name.replace(/^input/, "photo"); | |
| await writeFile(name, Buffer.from(await res.arrayBuffer())); | |
| console.log(`saved ${name}`); | |
| const res = await fetch(output.url); | |
| if (!res.ok) throw new Error(`download failed with ${res.status}`); | |
| const name = output.name.replace(/^input/, "photo"); | |
| await writeFile(name, Buffer.from(await res.arrayBuffer())); | |
| console.log(`saved ${name}`); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| } catch (error) { | ||
| await request(`/v1/jobs/${job.id}/cancel`, { method: "POST" }).catch(() => {}); | ||
| throw error; | ||
| } |
There was a problem hiding this comment.
🔴 Failed cancellation strands a job reservation
When cancellation fails after an upload or polling error, convert hides that failure and throws the original error. Callers receive no job ID to retry cancellation, leaving the reservation open until expiry.
Learn more
Creating a job reserves one cent, released by success, failure, cancellation, or expiry. After a failed upload, start, or poll, this catch tries to cancel it, but swallows any cancellation failure. The exported convert method then throws an error without the created job's ID, preventing callers from releasing a reservation that may remain open for up to 24 hours. The existing ConvtCancellationError shows how the SDK preserves the ID when cancellation cannot be confirmed.
Example: A successful reservation is followed by a network failure during upload; cancellation also receives a network error. The caller gets only the upload error, not the job ID needed to retry releasing its reservation.
Recommended fix: Surface a cancellation-unconfirmed error containing job.id whenever the cancel call fails, and preserve the original failure as its cause. Optionally inspect a successful cancel response for a terminal status.
Was this helpful? React with 👍 or 👎 to provide feedback.
| await sleep(pollMs); | ||
| current = await request(`/v1/jobs/${job.id}`, { signal }); |
There was a problem hiding this comment.
🟡 Conversion timeout ignores waiting periods
If timeoutMs expires during sleep, convert continues waiting until the next poll or retry. Large pollMs values can delay cancellation well past the configured deadline.
Learn more
The AbortSignal.timeout(timeoutMs) signal only reaches fetch requests. Neither the polling sleep here nor the retry sleep in request observes it. Expiry while waiting thus has no effect until the delay completes and the next fetch checks the aborted signal; cancellation is also postponed.
Example: With timeoutMs: 1000 and pollMs: 60000, a job that is still running after the start request holds convert for roughly 60 seconds instead of timing out after one.
Recommended fix: Make both sleeps abortable with the conversion signal and cancel the job immediately when the timeout fires; ensure the retry loop checks the signal before each delay.
Was this helpful? React with 👍 or 👎 to provide feedback.
| fi | ||
|
|
||
| # Outputs are named input.<ext>; save them under the input's own name. | ||
| STEM=$(basename "${FILE%.*}") |
There was a problem hiding this comment.
🟡 Dotted folders corrupt shell output names
For an extensionless input inside a dotted folder, STEM strips the folder suffix instead of the file extension. The script saves the conversion under the wrong name.
| STEM=$(basename "${FILE%.*}") | |
| STEM=$(basename "$FILE") | |
| [[ $STEM == *.* ]] && STEM=${STEM%.*} |
Was this helpful? React with 👍 or 👎 to provide feedback.
| "build": "bun scripts/generate-formats.ts && blume build", | ||
| "preview": "blume preview", | ||
| "doctor": "blume doctor", | ||
| "validate": "bun scripts/generate-formats.ts && blume validate", |
| 4. **Poll** the job until its status is `succeeded`, `failed` or `cancelled`. | ||
| 5. **Download** the outputs from the signed URLs the API returns. | ||
|
|
||
| The [Quick start](/quick-start) runs all five steps in Node, cURL, a browser app and the CLI. |
| export async function handleConvert(request) { | ||
| // Authenticate your own user here before spending your convt budget. | ||
|
|
||
| const form = await request.formData(); |
There was a problem hiding this comment.
|
Replace the single generated /docs/api page in apps/web with a Blume site in apps/docs: overview, quick start with Node, cURL, Browser and CLI tabs, guides, reference pages, examples, and an API reference rendered from convt-server's OpenAPI spec through an overlay. It deploys as the static convt-docs Worker on convt.app/docs*. Fixes CNV-38 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Outputs come back named input.<ext>, so the samples now rename them after the local file instead of overwriting each other. Polling guidance now stays under 120 requests a minute, start before upload is documented as the 502 it returns, expired jobs are documented as 404s, and the API reference links to the formats page that exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep batch conversions under the 120/min job-route limit with one shared request queue, and make the Railway host a single source of truth for samples, the OpenAPI overlay and Try it. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
d1e956c to
f698dc1
Compare
Guard the Bun demo server with import.meta.main, keep replace() from rewriting stems that contain $&, require a string API key in the SDK sample, and return job.id with outputs so later downloads have an id. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Summary
The only API docs were one generated page at
/docs/api, rendered from a thin spec with no guides, examples or error reference, and they pointed atapi.convt.app, which does not resolve. This replaces it with a Blume docs site inapps/docs, laid out like the TanStack docs, and deploys it to convt.app/docs.crates/convt-server/openapi.jsonthrough an overlay, so the server spec stays the source of truth. Each operation gets its own page, samples in cURL, Node, JS and Python, and a working Try it panel.cloud-formats.json, the same list the API checks.blume.config.ts. It is the Railway host for now, and every page notes thatapi.convt.appis not live (CNV-36).Evidence
convt-docsis live onconvt.app/docsandconvt.app/docs/*./docs,/docs/quick-start,/docs/api, every guide and example return 200, unknown paths return the docs 404, andwww.convt.app/docsredirects to the apex. The landing page and dashboard still come from convt-web.403 unauthorizedcorrectly. The output renaming was checked separately.blume build(strict),blume validate(no broken links),bun run check, andcheck-typesinapps/weball pass.input.<ext>, so the batch example overwrote its own results, and the polling advice would have hit the rate limit.Not tested: a full conversion with a real
cvt_live_key, since I had none. The flow matches@convt/sdk, which the server's replica acceptance test exercises.Follow-ups
502 storage_unavailableand leaves the job open. The docs describe the current behavior.apiBaseinapps/docs/blume.config.tsand runbun run --cwd apps/docs deploy.Merge Danger
Door: two-way. The docs Worker is already deployed; removing its two routes in Cloudflare hands
/docsback to convt-web, and reverting restores the old page.Blast radius:
convt.app/docsonly. Merging also drops/docs/apifrom convt-web, so staging (which has no docs Worker) will 404 there. CI's web job now installs Node 24 because Blume needs Node 22.19 or newer.Fixes CNV-38
Created with Claude Opus 5.5 in Claude Code.