Repository navigation
Expand file tree
/
Copy pathprogress.txt
More file actions
403 lines (355 loc) · 28.3 KB
/
Copy pathprogress.txt
File metadata and controls
403 lines (355 loc) · 28.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# Progress — @remocn/render-sdk
## PRD 01 — Workspace Skeleton (bun monorepo) — DONE (session 2354bde1)
### What was implemented
Converted the flat Fumadocs app into a bun workspace monorepo.
### Files changed / moved
- Moved as a unit into `apps/docs/`: `app/`, `content/`, `components/`, `lib/`,
`source.config.ts`, `next.config.mjs`, `postcss.config.mjs`, `proxy.ts`,
`tsconfig.json`, `README.md`, and the old root `package.json`.
- `apps/docs/package.json`: renamed package `remotion-render-sdk` -> `docs` (private,
deps/scripts unchanged).
- New root `package.json`: private workspace root, `workspaces: ["apps/*","packages/*"]`,
delegating scripts (`dev`, `build`, `docs:build`), `typescript` devDep only.
- `packages/.gitkeep`: empty slot for the SDK (PR 02).
- `.gitignore`: de-anchored `node_modules`, `.next/`, `coverage`, `out`, `build` (removed
leading `/`) so they ignore correctly inside `apps/docs` and future workspaces.
- Removed stale root `.source/` (regenerates under `apps/docs` via fumadocs-mdx postinstall).
### Verification evidence
- `bun install` at root: 564 packages, single hoisted store (`node_modules/.bun/`),
lockfile saved. `apps/docs/node_modules` holds only symlinks into the root store (no dup).
- `bun --filter docs build`: exit 0 — Compiled successfully, TypeScript finished, 12/12
static pages generated. `@/*` and `collections/*` aliases resolve from `apps/docs`.
- Root clean: no `app/`, `content/`, `next.config.mjs`, `source.config.ts` at root.
- `grep 'from "@/'` outside `apps/docs`: none.
### Learnings / patterns for future PRDs
- bun workspaces use an isolated linker: real packages live in root `node_modules/.bun/`,
each workspace gets a `node_modules/` of symlinks. "Single hoisted node_modules" = this.
- Docs configs are fully self-contained (aliases `@/* -> ./*`, `collections/* -> ./.source/*`),
so moving the dir as a unit needed zero path edits.
- `.source/` is generated by `fumadocs-mdx` postinstall; never commit/move it.
- Assistant does NOT run git (user owns all branches/commits/PRs). Next: PR 02 package scaffold.
## PRD 02 — Package Scaffold + Build Tooling — DONE (session f9fe16bb)
### What was implemented
Stood up the `@remocn/render-sdk` package shell + build pipeline. No SDK logic — placeholder
entrypoints that build green prove the toolchain for PRs 03–08.
### Files created (packages/render-sdk/)
- `package.json`: `@remocn/render-sdk` v0.0.0, type module, sideEffects false, subpath exports
`.`/`./server`/`./lambda` (types+import -> dist/...). deps: p-limit ^6.1.0. optional peer
deps @remotion/renderer & @remotion/lambda (>=4.0.0) also as devDeps. devDep @types/bun.
scripts build/dev(--watch)/test/types. publishConfig.access public.
- `tsconfig.json`: strict, module/moduleResolution bundler, verbatimModuleSyntax, noEmit,
lib [ESNext, DOM], `types: ["bun"]`. include src/scripts/test.
- `tsconfig.build.json`: extends base, declaration + emitDeclarationOnly, outDir dist,
rootDir src, noEmit false, include [src], exclude scripts/test.
- `bunfig.toml`: [test] coverage=false.
- `scripts/build.ts`: derives entrypoints from pkg.exports, cleans dist, Bun.build (target
node, format esm, root=src so subdir layout mirrors src), external = peers+deps+node
builtins (bare + node: forms), then `tsc -p tsconfig.build.json` for .d.ts. `--watch` loop.
- `src/index.ts` (`export const VERSION = "0.0.0"`), `src/server/index.ts`, `src/lambda/index.ts`
(`export {}`).
- `test/build-output.test.ts`: asserts VERSION === "0.0.0" (gives bun test a file -> exit 0).
- `README.md` stub.
### Verification evidence (all exit 0)
- `bun install` (root): resolves, remotion peers + p-limit + @types/bun in lock, lockfile saved.
- `bun run build`: dist/{index,server/index,lambda/index}.js + matching .d.ts (6 files).
- `bun run types` (tsc --noEmit): 0 errors.
- `bun test`: 1 pass, 0 fail.
- externals: `grep -rE "p-limit|@remotion" dist` -> none bundled (placeholders import nothing).
- `node import('./dist/index.js')` -> VERSION=0.0.0.
### Learnings / patterns for future PRDs
- bun isolated linker symlinks @types/bun into packages/render-sdk/node_modules/@types/bun,
but TS auto-@types inclusion did NOT register its globals (Bun/process/node:*). Fix:
explicit `"types": ["bun"]` in tsconfig.json — @types/bun pulls @types/node + bun-types.
- `bun test` with zero test files exits non-zero ("0 test files matching"). Ship at least one
trivial test (or the build-output test) so the test gate is green.
- Importing `../src/index.ts` with the `.ts` extension fails tsc (TS5097) unless
allowImportingTsExtensions; drop the extension — bundler resolution finds it.
- Bun.build `root: srcDir` is what preserves the dist/server, dist/lambda subdir layout so
subpath export paths line up; without it everything flattens to dist/.
- Next: PR 03 (core types, errors, handle, codecs, SPI) writes into src/ — packaging frozen.
## PRD 03 — Core: Types, Errors, Handle, Codecs, Adapter SPI — DONE (session d7db0d3f)
### What was implemented
The dependency-free core surface — the contract every later PR (store/sdk/adapters) imports.
Zero `@remotion/*` references anywhere in src, so the root export's .d.ts stays remotion-free.
### Files created (packages/render-sdk/src/)
- `types.ts`: RenderStatus, branded RenderHandle (`string & {readonly __brand: unique symbol}`),
own Codec union (h264,h265,vp8,vp9,gif,prores,mp3,aac,wav), PixelFormat (8 fmts), RenderState,
RenderInput (compositionId required; codec defaults to h264 — documented), WaitOptions.
- `errors.ts`: RenderError extends Error; readonly `code` union (invalid_input|render_failed|
timeout|not_found|version_mismatch|adapter_error); ctor(code,message,{cause?}) sets name +
forwards cause to super.
- `handle.ts`: `~`-delimited, adapter-tag-first scheme. encodeServerHandle -> `s~jobId`;
encodeLambdaHandle -> `l~renderId~bucket~ext`; decodeHandle returns DecodedHandle union, throws
RenderError("not_found") on garbage / wrong arity / blank fields. Fixed 4 segments for lambda
so a bucket containing "." round-trips.
- `codecs.ts`: CODEC_EXT exhaustive Record<Codec,string>; extForCodec(codec?) defaults h264->mp4.
- `adapter.ts`: RenderAdapter<TOptions> SPI (start/getState/getUrl/download) + OptionsOf<A> helper.
- `index.ts`: kept VERSION="0.0.0"; re-exports the full public surface (type-only where types).
- Tests: handle.test.ts (round-trip both adapters, "." bucket, 10 garbage cases all -> not_found),
codecs.test.ts (full map + default), errors.test.ts (instanceof/code/cause/all codes).
### Verification evidence (all exit 0)
- `bun run types` (tsc --noEmit): 0 errors.
- `bun run build`: exit 0; dist now 11 files (index + 5 core .d.ts + server/lambda + 3 .js).
- `bun test`: 20 pass, 0 fail.
- `grep -rl "@remotion" dist`: none (root .d.ts remotion-free confirmed).
- node import of built dist/index.js: decode/extForCodec/RenderError instanceof all work.
### Learnings / patterns for future PRDs
- Branded RenderHandle breaks `expect(handle).toBe("s~abc")` — bun's toBe infers `expected` from
the branded actual (NoInfer<RenderHandle>), so a plain string literal isn't assignable. Cast the
actual: `expect(handle as string).toBe(...)`. Runtime value is a normal string.
- tsc preserves JSDoc into .d.ts, so a comment containing the literal `@remotion` will trip a
`grep "@remotion" dist`. Keep the "remotion-free" rationale in comments without the `@remotion`
token (lowercase "remotion" is fine).
- decodeHandle uses split("~") with fixed segment counts (server=2, lambda=4) + a blank-field
guard (trim().length===0) — this is what rejects `s~` and `l~r~ ~mp4` while letting `my.bucket`
through. region/functionName intentionally NOT in the handle (come from adapter config).
- Next: PR 04 (StateStore + InMemoryStore) and PR 05 (RenderSdk + waitForCompletion) import these
core types — do not redefine them.
## PRD 04 — State Store SPI + InMemoryStore — DONE (session d7db0d3f)
### What was implemented
The progress source-of-truth SPI (server adapter writes, getState reads; lambda does not use it)
plus the default Map-backed InMemoryStore. Remotion-free, sync-in/async-out.
### Files created (packages/render-sdk/src/store/)
- `types.ts`: RenderRecord { status, progress (0..1), error?, codec, createdAt, meta? } imported
RenderStatus/RenderHandle/Codec from ../types (no redefinition). StateStore interface:
create/get/update/delete, all Promise-returning. JSDoc documents semantics (overwrite, null,
not_found, idempotent delete).
- `in-memory.ts`: InMemoryStore(): StateStore over `new Map<string,RenderRecord>()` keyed by raw
handle. create overwrites; update spreads {...existing, ...patch} or throws
RenderError("not_found") on missing; get -> record ?? null; delete via Map.delete (idempotent).
- `index.ts`: added `export { InMemoryStore }` + `export type { StateStore, RenderRecord }`.
- `test/in-memory.test.ts`: CRUD lifecycle, partial-update field preservation, missing-update
not_found, get-missing null, idempotent double-delete, createdAt/meta round-trip, overwrite,
store-instance independence (8 tests).
### Also (user request, PRD 03 handle.ts, behavior unchanged)
- Extracted decodeHandle inline validation into named guards: rejectEmptyHandle,
rejectMalformedServerHandle, rejectMalformedLambdaHandle, rejectUnknownAdapterTag. decodeHandle
now reads as a sequence of named rejects. All 28 tests still green.
### Verification evidence (all exit 0)
- `bun run types`: 0 errors. `bun run build`: 0; dist now 13 files (+store/types.d.ts,
store/in-memory.d.ts). `bun test`: 28 pass, 0 fail. `grep @remotion dist`: none.
- node import of built dist: InMemoryStore is a function; create/update/get/delete + not_found
throw all behave per spec.
### Learnings / patterns for future PRDs
- Bun.build `root: src` keeps the dist/store subdir layout (mirrors dist/server, dist/lambda) so
new subdirs just work — no build-script change needed when adding src subfolders.
- InMemoryStore stores records by reference (update replaces with a fresh spread object). Fine for
v1 single-process; a Redis/SQL StateStore would serialize. Returned records are snapshots —
callers must go through update() to persist mutations.
- Next: PR 05 (RenderSdk + waitForCompletion) wires the default InMemoryStore; PR 06 (server
adapter) writes RenderRecords. Both import StateStore/RenderRecord — do not redefine.
## PRD 05 — RenderSdk + waitForCompletion — DONE (session a236fb66)
### What was implemented
The public `RenderSdk<A>` facade + the adapter-agnostic `waitForCompletion` poll loop. Both
remotion-free, verified against fake/scripted in-memory adapters (no real backend yet).
### Files created (packages/render-sdk/src/)
- `wait.ts`: standalone `waitForCompletion(adapter: Pick<RenderAdapter<unknown>,"getState">,
handle, opts?)`. `for(;;)` poll loop: checks signal -> getState -> onProgress(progress) ->
done returns final state / error throws RenderError("render_failed", state.error ?? "Render
failed") / past deadline throws RenderError("timeout") -> abortableDelay(min(intervalMs,
deadline-now), signal). `abortableDelay` is a setTimeout wrapped in a Promise with a once
"abort" listener that clearTimeouts + rejects RenderError("adapter_error","aborted") — this is
what makes abort interrupt MID-WAIT, not just between polls. DEFAULT_INTERVAL_MS = 1000.
- `sdk.ts`: `class RenderSdk<A extends RenderAdapter<any>>`, ctor({ adapter, store? }) stores both
as readonly fields. start(input, options?: OptionsOf<A>) / getState / getUrl / download are
one-line delegations to this.adapter; waitForCompletion(handle, opts?) delegates to the
standalone loop with this.adapter. Exports `RenderSdkConfig<A>` type. The SDK never reads/writes
the store — it only holds it for adapters (server adapter, PR 06, will use it).
- `index.ts`: added `export { RenderSdk, type RenderSdkConfig } from "./sdk"` and
`export { waitForCompletion } from "./wait"`.
### Tests (test/)
- `wait.test.ts`: scripted getState-only adapter (queued->rendering(.5)->done) asserts onProgress
[0,.5,1] + resolution; separate cases throw render_failed on error, timeout on timeoutMs, abort
mid-wait via AbortController, and immediate reject on already-aborted AbortSignal.abort().
- `sdk.test.ts`: fakeAdapter records a discriminated `Call[]`; verifies all 5 methods delegate,
store is held (sdk.store === store) but untouched (store.get null after start), store optional,
and a `@ts-expect-error` case proving `start`'s 2nd arg narrows to OptionsOf<A> (wrong-shape
`{ chromiumOptions: {} }` fails to compile).
### Verification evidence (all exit 0)
- `bun run types` (tsc --noEmit): 0 errors. `bun run build`: succeeded; dist now 15 files
(+sdk.{js,d.ts}, +wait.{js,d.ts}). `bun test`: 39 pass, 0 fail (was 28). `grep -rl @remotion
dist`: none. Architect reviewer: APPROVED against all PRD-05 acceptance criteria. Deslop pass:
removed one redundant monotonic assertion in wait.test.ts; post-deslop regression still 39 pass.
### Learnings / patterns for future PRDs
- `@ts-expect-error` IS the type-narrowing test: because tsc errors on an UNUSED @ts-expect-error
directive, a green `bun run types` proves the wrong-shape option genuinely fails to compile. No
separate expectTypeOf/.test-d harness needed — the directive self-verifies under the existing
types gate (test/ is in tsconfig include).
- waitForCompletion is intentionally `Pick<RenderAdapter,"getState">`, not the full adapter — keeps
it trivially testable with a one-method fake and documents that polling only needs getState.
- Error contract is THROW (not resolve): done -> resolve(state); error -> throw render_failed;
timeout -> throw timeout; abort -> throw adapter_error/"aborted". getState itself always resolves
a state object; the loop is what converts terminal states into resolve/throw.
- RenderSdk is a pure thin delegator: it holds `store` but never calls it. Store writes are owned by
the server adapter (PR 06), which receives the store via its OWN config, not via the SDK. Confirm
this wiring direction when implementing PR 06 — do not make the SDK seed the store.
- Next: PR 06 (server adapter, depends 03/04/05) + PR 07 (lambda adapter, depends 03/05) are now
unblocked and independent — parallel branches. Both plug into a real `new RenderSdk({ adapter })`.
## PRD 06 — Server Adapter (`@remocn/render-sdk/server`) — DONE (session 90eeeca7)
### What was implemented
`RenderServer(config)` — the in-process adapter over `@remotion/renderer` with a `p-limit`
limiter, store-backed progress, disk output, and getUrl/download from disk. Remotion stays an
optional peer dep imported only by the /server subpath (root export remains remotion-free).
### Files created / changed (packages/render-sdk/)
- `src/server/index.ts`: RenderServer(config: ServerConfig): RenderAdapter<ServerOptions>.
ServerConfig{serveUrl, workDir(req), publicUrl?, concurrency?(def 2), store?(def fresh
InMemoryStore)}; ServerOptions{concurrency?, chromiumOptions?, timeoutInMilliseconds?}.
start() → codec(default h264) → encodeServerHandle(crypto.randomUUID()) → store.create(queued)
→ `void limit(() => runRender(...))` DETACHED → returns handle. runRender: store.update(rendering)
→ selectComposition → mkdir -p workDir → renderMedia({outputLocation:`${workDir}/${handle}.${ext}`,
onProgress→store.update(clamp01(p.progress))}) → store.update(done,1); catch → store.update(error,
message, errorCode=classify). getState maps record→{status,progress,error?}, missing→not_found.
getUrl: publicUrl ? `${publicUrl}/${handle}.${ext}` : `${handle}.${ext}` (trailing slash trimmed),
ext from stored codec. download: stat file → Readable.toWeb(createReadStream) as unknown as
ReadableStream; missing record/file → not_found.
- `src/store/types.ts`: additive optional `errorCode?: RenderError["code"]` on RenderRecord (the
channel that surfaces version_mismatch vs render_failed — RenderState has no code field).
- Tests (6 files + shared `test/_server-harness.ts`): server.start / .limiter / .url / .download /
.error / .types. Harness mocks `@remotion/renderer` via `mock.module` + dynamic
`await import("../src/server")`, with mutable `remotion.selectImpl/renderImpl` swapped per test,
plus deferred()/flush()/makeWorkDir()/seedDone()/expectRenderErrorCode() helpers.
### Verification evidence (all exit 0)
- `bun run types`: 0 errors. `bun run build`: success. `bun test`: 56 pass / 0 fail across 13 files
(was 39/7). dist/server/index.js keeps `import { renderMedia, selectComposition } from
"@remotion/renderer"` external (0 remotion internals bundled). Architect reviewer: APPROVED
against all PRD-06 acceptance criteria. Deslop pass: consolidated duplicate `seed` helper into
harness `seedDone`, trimmed fakeComposition to 5 fields; post-deslop regression still 56 pass.
### Learnings / patterns for future PRDs
- Mocking a statically-imported native peer dep in bun: register `mock.module("@remotion/renderer",
factory)` at the TOP of the test file, THEN `const { RenderServer } = await import("../src/server")`
— static top imports run before top-level code, so the adapter must be dynamically imported AFTER
the mock. Factory delegates to a mutable `remotion` object so each test swaps impls without
re-registering. Each test file registers its own mock.module; works across the 13-file suite.
- `Readable.toWeb()` returns a `node:stream/web` ReadableStream that TS treats as nominally distinct
from the global `ReadableStream` (getReader overload arity differs) — `as unknown as ReadableStream`
is the sanctioned bridge; identical at runtime.
- remotion v4 (4.0.484): ENABLE_V5_BREAKING_CHANGES=false so licenseKey is OPTIONAL — renderMedia
needs only {codec, composition, serveUrl}. selectComposition returns Promise<VideoConfig> directly.
remotion's validPixelFormats == our PixelFormat union exactly, and our Codec ⊂ remotion Codec, so
input.codec/pixelFormat forward without casts (codec type-guard test locks this).
- Detached render: failures are caught INSIDE runRender and written to the store, so the unawaited
`limit(...)` promise never rejects (no unhandledRejection). Concurrency is provable deterministically
by gating renderMedia with a deferred() under concurrency:1 — 2nd job's runRender never starts, so
its record stays `queued`.
- errorCode lives on RenderRecord, not RenderState (kept the public poll shape unchanged). version
classification is reactive: /version/i && /match/i on the message → version_mismatch, else
render_failed (matches the locked design record — no proactive version probing).
- Next: PR 07 (lambda adapter, depends 03/05) is the sibling backend; PR 08 (docs) waits on both
adapters; PR 09 (CI) can land anytime after 02.
## PRD 07 — Lambda Adapter (`@remocn/render-sdk/lambda`) — DONE (session f5a19b9f)
### What was implemented
`RenderLambda(config)` — the distributed adapter over `@remotion/lambda` (v4.0.484). Fires renders
immediately (no p-limit, no store — AWS scales), reads state from `getRenderProgress`, encodes all
per-job data into the handle (`l~renderId~bucket~ext`), and maps Remotion's version error to
`RenderError("version_mismatch")`. `@remotion/lambda` stays an optional peer dep imported only by
the /lambda subpath (root export stays remotion-free, like /server).
### Files created / changed (packages/render-sdk/)
- `src/lambda/index.ts` (was `export {}`): RenderLambda(config: LambdaConfig): RenderAdapter<LambdaOptions>.
LambdaConfig{region: AwsRegion, functionName, serveUrl}; LambdaOptions{framesPerLambda?,
webhook?:{url;secret}, privacy?:"public"|"private", outName?, maxRetries?}. start() → codec(default
h264) → try renderMediaOnLambda({region,functionName,serveUrl:input.serveUrl??config.serveUrl,
composition:input.compositionId, codec, inputProps, pixelFormat, frameRange, scale, jpegQuality,
+ 5 options}) → {renderId,bucketName} → encodeLambdaHandle({renderId,bucket:bucketName,
ext:extForCodec(codec)}); catch → throw RenderError(classifyRenderError(message), ..., {cause}).
Shared fetchProgress(handle) = decodeHandle + lambda-guard(not_found) + getRenderProgress({renderId,
bucketName:bucket, functionName, region}). mapProgress: done→{done,1}; fatalErrorEncountered→{error,
overallProgress, errors[0]?.message (conditional spread)}; overallProgress>0→{rendering,overallProgress};
else→{queued,0}. getUrl: fetchProgress → outputFile ?? throw not_found. download: fetch(getUrl) →
!ok||!body ? throw adapter_error : res.body.
- `test/_lambda-harness.ts` + 5 specs (start/state/url-download/version/types): 24 new tests.
mockLambda() registers mock.module("@remotion/lambda/client") BEFORE dynamic `await import("../src/lambda")`.
makeProgress(partial) returns only the 5 fields the adapter reads (deslop: dropped ~30 unused
RenderProgress fields). lambda.types.test.ts: Codec→ServerlessCodec assignability + @ts-expect-error
proving chromiumOptions (server-only) fails to compile through RenderSdk.
### Verification evidence (all exit 0)
- `bun run types`: 0 errors. `bun run build`: succeeded. `bun test`: 80 pass / 0 fail across 18 files
(was 56/13). `grep -c 'from "@remotion/lambda/client"' dist/lambda/index.js` = 1; remotion internals
bundled = 0 (external preserved). Architect reviewer: APPROVED against all PRD-07 acceptance criteria.
Deslop: trimmed makeProgress to the 5 read fields; post-deslop regression still 80 pass.
### Learnings / patterns for future PRDs
- @remotion/lambda v4.0.484: import { renderMediaOnLambda, getRenderProgress } AND `type AwsRegion`
from the `@remotion/lambda/client` subpath. renderMediaOnLambda takes `composition` as the id STRING
(not a VideoConfig object — unlike server's selectComposition). Returns {renderId, bucketName, ...}.
- serverlessCodecs == our Codec union EXACTLY (h264,h265,vp8,vp9,mp3,aac,wav,gif,prores), so input.codec
forwards to renderMediaOnLambda with no cast. PixelFormat + frameRange [n,n] also forward cast-free.
- WebhookOption/Privacy/OutNameInput are NOT re-exported from @remotion/lambda/client (they live in
@remotion/serverless-client). Type LambdaOptions structurally ({url;secret}/"public"|"private"/string) —
these are assignable to what renderMediaOnLambda accepts, keeping the adapter's public types lean.
- getRenderProgress (GenericRenderProgress): the fields that matter are done, overallProgress,
fatalErrorEncountered, errors[].message, outputFile (string|null). outputFile is null until done.
- classifyRenderError (/version/i && /match/i → version_mismatch) already covers Remotion's lambda
version-mismatch message ("...version does not match...") — reused, no lambda-specific matcher needed.
- Lambda is stateless: start does NOT await render completion; renderMediaOnLambda returns after the
render is enqueued on AWS, so returning the handle immediately is correct (no detached promise like
server's `void limit(...)` — the cloud owns execution).
- Next: PR 08 (docs + README) is now unblocked (both adapters frozen). PR 09 (CI) can land anytime.
## PRD 08 — Docs + README — DONE (session f5a19b9f)
### What was implemented
Documented the frozen public surface: package README + 7 Fumadocs pages under apps/docs. Docs-only
PR (no src/ touched). Since the SDK ships no HTTP/provisioning, the docs carry the transport +
provisioning recipes as copy-paste (explicitly the consumer's job).
### Files created / changed
- `packages/render-sdk/README.md` (replaced stub): what-it-is, install (+ optional peers
@remotion/renderer / @remotion/lambda), server + lambda quickstarts, full API tables (RenderSdk
methods, RenderInput/WaitOptions/RenderState, RenderError codes, InMemoryStore factory, handle
helpers, codec utils, both adapters, standalone waitForCompletion, provisioning).
- `apps/docs/content/docs/`: index.mdx (overview + responsibility boundary, ascii flow, adapter table,
nav Cards), public-surface.mdx (RenderSdk<A> + every method signature + getUrl table + RenderError
table + InMemoryStore + handle helpers + codec utils + full type reference), adapters.mdx
(RenderServer/RenderLambda config+options+runtime behavior, server-vs-lambda comparison table,
unified getUrl table, RenderAdapter interface), transport-recipes.mdx (shared sdk, POST start route,
GET poll route, GET download route, browser poll loop), provisioning.mdx (bundle for server;
deploySite/deployFunction/getOrCreateBucket for lambda — consumer's job), state-store.mdx (StateStore
SPI + InMemoryStore default + Redis/SQL swap), limitations.mdx (out-of-scope: list/cancel/delete/
upload/HTTP; server + lambda + general constraints).
- `apps/docs/content/docs/meta.json`: nav order index→public-surface→adapters→transport-recipes→
provisioning→state-store→limitations.
- `apps/docs/app/(home)/page.tsx`: replaced "Hello World" starter with SDK copy + /docs link.
- Removed starter `apps/docs/content/docs/test.mdx`.
### Verification evidence
- `bun --filter docs build`: exit 0, all 7 docs pages prerendered (SSG), nav renders in order.
- `grep cancelRender|listRenders|deleteRender|uploadAsset` over docs+README: empty (no out-of-scope).
- code-reviewer: APPROVED — every snippet matches the shipped API (real module specifiers, real export
names, InMemoryStore() factory not `new`, correct method signatures, honest getUrl/boundary claims).
- Deslop: fixed prose naming drift in index.mdx (`ServerAdapter`/`LambdaAdapter` → real
`RenderServer`/`RenderLambda`); post-deslop docs build still exit 0.
### Learnings / patterns for future PRDs
- Fumadocs (this app): every `content/docs/*.mdx` needs frontmatter `title` + `description` (pageSchema)
or the build fails. Nav is `content/docs/meta.json` `{ "title", "pages": [...basenames] }`. `Cards`/`Card`
are available in MDX with no import (registered in mdx-components). Avoid Tabs/Steps unless you import
from `fumadocs-ui/components/*` and verify the build. Fenced code blocks are NOT type-checked by the
build — accuracy is enforced by review, not the compiler.
- Docs build = `bun --filter docs build` (~2 min). It's the only gate for a docs PR; runs the full Next
build incl. llms.txt + OG image routes.
- Honest-boundary lines that must persist in docs: getUrl valid only when status==="done"; server
publicUrl is just a base string (SDK serves nothing — expose workDir yourself); lambda getUrl throws
not_found before done; InMemoryStore() is a factory.
- Next: PR 09 (CI / GitHub Actions, depends only on 02) is the last remaining PRD — runs types/build/test
for packages/render-sdk + the docs build.
================================================================
[CI/CD ralph run — 2026-06-28] Biome+ultracite lint/format, tsc, changesets+OIDC
================================================================
Engine decision: single Biome (2.4.16) across monorepo.
- ultracite@7 changed engine: <=6 = Biome preset; 7.x = oxlint/oxfmt.
Used `ultracite init --linter biome` so docs stays on Biome (extends ultracite/biome/core).
- docs: apps/docs/biome.jsonc (extends ultracite/biome/core) + rule overrides
(useAwait/noBarrelFile/noDescendingSpecificity off — Next/Tailwind conventions).
lint='ultracite check', format='ultracite fix'. Added apps/docs/.gitignore
(ultracite core enables vcs.useIgnoreFile -> needs .gitignore; also excludes .omc/).
- render-sdk: packages/render-sdk/biome.json standalone (recommended rules, 2-space,
double quotes, semicolons). lint='biome ci .'. Removed vcs block (no per-pkg .gitignore).
bun --filter GOTCHA (bun 1.3.2): `bun --filter <name> run <script>` (space form + run)
fails "No packages matched". Reliable form: `bun --filter='<sel>' run <script>` (equals).
Root scripts standardized to equals form. Single pkg selector: '@remocn/*'.
Changesets: .changeset/config.json -> changelog-github {repo Remocn/render-sdk},
access public, baseBranch main. docs is private -> auto-excluded (verified via temp
changeset: only @remocn/render-sdk bumps).
Workflows: ci.yml (PR + push main, bun 1.3.2 pinned, fetch-depth 0,
install->lint->typecheck->test->build->soft changeset status).
release.yml (push main, id-token:write, setup-node 22 + npm@latest for OIDC>=11.5.1,
changesets/action@v1, NPM_CONFIG_PROVENANCE, no NPM_TOKEN).
Verification (all green): bun run lint=0 (render-sdk 9 warnings non-blocking, docs clean),
typecheck=0, test=0 (80 tests), build=0 both. YAML valid.
Manual prereqs (RELEASE.md): 1) first manual npm publish to claim name,
2) npmjs Trusted Publisher (Remocn/render-sdk, release.yml), 3) enable Actions-create-PRs,
4) optional branch protection.