|
| 1 | +# WritableStream, WritableStreamDefaultWriter, WritableStreamDefaultController |
| 2 | + |
| 3 | +An informal specification of the JS-backed writable stream classes as |
| 4 | +implemented in workerd, derived from — and kept in lockstep with — the test |
| 5 | +suite in this directory. **The tests are the normative artifact.** Both the |
| 6 | +C++ implementation (`src/workerd/api/streams/writable.{h,c++}` over |
| 7 | +`standard.c++`'s `WritableImpl`/`WritableStreamJsController`) and the |
| 8 | +TypeScript implementation (`src/per_isolate/webstreams/writable.ts`, behind |
| 9 | +`typescript_implemented_streams`) are covered. |
| 10 | + |
| 11 | +The suite COMPLEMENTS WPT (`//src/wpt:streams` runs writable-streams/ |
| 12 | +against both implementations): behaviors WPT already asserts identically on |
| 13 | +both sides are not duplicated here. The WPT C++ expectedFailures for |
| 14 | +writable-streams/ in `src/wpt/streams-test.ts` (aborting ×11, |
| 15 | +bad-strategies ×2, bad-underlying-sinks ×2, constructor ×2, floating-point |
| 16 | +×3, start ×1) seeded the ledger below; probing showed several "failures" |
| 17 | +(the aborting.any family) come from testharness strictness while the |
| 18 | +underlying behaviors are parity — those parity pins live in |
| 19 | +`abort-matrix.js`. |
| 20 | + |
| 21 | +## The startedness model (root of several divergences) |
| 22 | + |
| 23 | +C++ invokes sink algorithms synchronously from `writer.write()`/`close()` |
| 24 | +(without `pedantic_wpt`; with it they are deferred a microtask, which does |
| 25 | +NOT change the outcomes below). TypeScript follows the spec's `[[started]]` |
| 26 | +gating: no sink hook runs until the start promise has settled in a |
| 27 | +microtask. A synchronous `write(); abort();` sequence therefore finds the |
| 28 | +write already in flight under C++ (the sink runs; the write fulfills) but |
| 29 | +still queued under TypeScript (the sink never runs; the write rejects with |
| 30 | +the abort reason). |
| 31 | + |
| 32 | +## Divergence ledger (C++ vs TypeScript) |
| 33 | + |
| 34 | +| # | Area | C++ | TypeScript | Pinned in | |
| 35 | +| --- | --- | --- | --- | --- | |
| 36 | +| 1 | invalid highWaterMark at ctor | TypeError (jsg uint64 conversion messages) | RangeError "Invalid highWaterMark" (spec) | `highWaterMarkValidated` | |
| 37 | +| 2 | sink.type validation | ignored (pedantic_wpt: RangeError) | RangeError "Invalid underlying sink type" (spec) | `sinkTypeValidation` | |
| 38 | +| 3 | argument conversion order | sink dictionary first (sink.write, hwm, size) | strategy first (size, hwm ×2, sink.write; spec) | `argumentConversionOrder` | |
| 39 | +| 4 | ready at construction (no backpressure) | pending after a microtask, fulfills on a later turn | fulfilled within a microtask (spec) | `readyFulfillTiming` | |
| 40 | +| 5 | sync start() throw | captured; stream errored, writes reject | escapes the constructor (spec) | `newWritableStreamStartError` | |
| 41 | +| 6 | abort() on an errored stream | rejects with the stored error | fulfills with undefined (spec) | `newWritableStreamAbortError` | |
| 42 | +| 7 | startedness (see above) | sync write→abort: sink runs, write fulfills | write still queued: rejected with abort reason | `writableStreamAbortWhileWriting`, `writableStreamAbortWriteClosePending`, `writableStreamPromisesResolvedInOrder`, `writableStreamCloseThrowRejectsPromises` | |
| 43 | +| 8 | close hook racing an immediate abort | close hook runs; close+abort reject with its error | close still queued: hook never runs, close rejects with abort reason, abort fulfills | `writableStreamCloseThrowRejectsPromises` | |
| 44 | +| 9 | queue totals | size() → uint64 (fractions truncate; NaN/negative/±Infinity → TypeError); desiredSize narrowed through `int` (wraps past 2^31) | double arithmetic per spec; invalid size → RangeError "Invalid chunk size" | `floatingPointQueueTotals`, `fractionalSizeTruncation`, `invalidSizeReturnRejects` | |
| 45 | +| 10 | signal.reason for reasonless abort() | undefined (pedantic_wpt: AbortError DOMException) | AbortError DOMException (spec) | `abortSignalReason` | |
| 46 | +| 11 | desiredSize while erroring | queue accounting value (pedantic_wpt: null) | null (spec) | `desiredSizeWhileErroring` | |
| 47 | +| 12 | non-callable size / released-writer messages | jsg dictionary / "This WritableStream writer has been released." | TS validator / "This writer has been released" | `nonCallableSizeThrows`, `releaseLockInsideSize` | |
| 48 | + |
| 49 | +Parity worth noting (probed, pinned): the whole in-flight abort matrix — |
| 50 | +abort-before-start reason identity on ready/closed, errored-state reason |
| 51 | +identity, sink abort suppressed after a bad-strategy error or a |
| 52 | +pre-existing controller error, in-flight write finishing with rejection |
| 53 | +during abort, both orders of `abort()`×`controller.error()` during an |
| 54 | +in-flight write, sink abort waiting for in-flight start/write/close |
| 55 | +(`abort-matrix.js`, `writableStreamAbortTiming`); reentrant |
| 56 | +`writer.write()` from size() enqueues the inner chunk first; size() is |
| 57 | +never consulted for doomed writes (the TS sinks' `willAcceptWrite` |
| 58 | +invariant); a patched `Object.prototype.then` getter never fires while |
| 59 | +settling writer promises (they resolve with undefined). |
| 60 | + |
| 61 | +## Compatibility flags |
| 62 | + |
| 63 | +| Flag (enable date) | Pinned in main cells | Unflagged side guarded by | |
| 64 | +| --- | --- | --- | |
| 65 | +| `streams_enable_constructors` (2022-11-30) | yes (the suite's subject) | `writable-cpp-legacy` cell: ctor throws a flag-pointing Error; `WritableStreamDefaultController` global absent | |
| 66 | +| `capture_async_api_throws` (2022-10-31) | yes | `writable-cpp-legacy-writer` cell: double close throws synchronously ("Cannot close a writer that is already being closed") | |
| 67 | +| `writable_stream_spec_compliant_writer` (2026-03-24) | yes | `writable-cpp-legacy-writer` cell: releaseLock() inside size() does not doom the write; release leaves a resolved ready in place | |
| 68 | +| `workers_api_getters_setters_on_prototype`, `set_tostring_tag` | yes | generic placement/branding; identity suite legacy cell | |
| 69 | +| `internal_writable_stream_abort_clears_queue` (2024-09-02) | NOT pinned | internal-impl only (`internal.c++`); unreachable from JS-backed WritableStream | |
| 70 | +| `pedantic_wpt` (dateless opt-in) | `writable-cpp-pedantic` cell | flag-off sides are the C++ columns of ledger #2/#10/#11 | |
| 71 | + |
| 72 | +## Assertion catalogue |
| 73 | + |
| 74 | +| Module | Asserts | |
| 75 | +| --- | --- | |
| 76 | +| `api-surface.js` | writable globals exist; controller not constructable; bare ctor works (full IDL shape is WPT's) | |
| 77 | +| `construction.js` | ledger #1–#5, #12; fractional hwm accepted | |
| 78 | +| `sink-algorithms.js` | which sink hooks run with what arguments/controller; sync+async hook errors surface on writer promises (#5, #6); size() consulted per write | |
| 79 | +| `write-semantics.js` | chunk identity (subarrays, any JS value via Object.is); multiple pending writes; settlement ordering incl. under abort (#7) | |
| 80 | +| `close-semantics.js` | close-throw promise fan-out vs abort (#7, #8); double close rejects TypeError | |
| 81 | +| `abort-semantics.js` | migrated abort lifecycle: reason propagation, signal event, persistent errored state, in-flight sequencing, terminal-state interactions (#7) | |
| 82 | +| `abort-matrix.js` | probed parity matrix (see above) + signal reason (#10) | |
| 83 | +| `backpressure.js` | desiredSize accounting/recovery; ready replacement; WPT floating-point scenarios (#9); erroring desiredSize (#11) | |
| 84 | +| `reentrancy.js` | size()-reentrant write ordering; releaseLock inside size (#12; flag-gated, cf. legacy-writer); controller.error inside write hook; doomed-write size skip | |
| 85 | +| `then-interceptors.js` | then-getter never fires on writer promise settlement | |
| 86 | +| `gc.js` | pending write survives gc() with all user refs dropped (--expose-gc) | |
| 87 | +| `legacy-ctor-gate.js` | fully-unflagged: ctor Error + absent controller global | |
| 88 | +| `legacy-writer.js` | pre-flag writer semantics (see Compatibility flags) | |
| 89 | +| `which-impl.js` | implementation + pedantic detection | |
0 commit comments