Skip to content

Commit 2296f1b

Browse files
committed
Document the writable streams suite
1 parent d499eee commit 2296f1b

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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

Comments
 (0)