@statewalker/webrun-streams-conformance 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -1
- package/dist/describe-duplex-adapter.d.ts +1 -1
- package/dist/describe-duplex-adapter.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -1
- package/dist/loopback.d.ts +10 -1
- package/dist/loopback.d.ts.map +1 -1
- package/package.json +12 -6
- package/src/describe-duplex-adapter.ts +51 -1
- package/src/index.ts +6 -1
- package/src/loopback.ts +11 -1
package/README.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Conformance suite for `Duplex` / `Connect` / `Serve` adapters in the `webrun-streams-*` family. Every adapter ships a one-line test file that calls `describeDuplexAdapter(name, makePair)` with its own pair factory.
|
|
4
4
|
|
|
5
|
+
## Why it exists
|
|
6
|
+
|
|
7
|
+
The promise of the [`webrun-streams`](../webrun-streams) seam is that a handler
|
|
8
|
+
written once runs over *any* transport. That promise is only as good as the
|
|
9
|
+
weakest adapter — and the failure modes that break it are the quiet ones: an
|
|
10
|
+
adapter that works for small bodies but truncates at 10 MiB, that serialises
|
|
11
|
+
concurrent calls, that drops a handler's `finally` on cancellation, or that
|
|
12
|
+
turns a thrown `Error` into an anonymous disconnect.
|
|
13
|
+
|
|
14
|
+
None of those show up in an adapter's own happy-path tests. This package makes
|
|
15
|
+
them a shared, executable definition of "correct", so a new transport is a
|
|
16
|
+
day's work rather than a new set of subtle incompatibilities.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install --save-dev @statewalker/webrun-streams-conformance
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
It is a **test-time** dependency: it bundles `vitest` and defines suites via
|
|
25
|
+
`describe` / `it`.
|
|
26
|
+
|
|
5
27
|
## Levels asserted
|
|
6
28
|
|
|
7
29
|
- **L0** Envelope round-trip via an echo handler for body sizes empty / 1 KiB / 1 MiB / 10 MiB.
|
|
@@ -10,6 +32,7 @@ Conformance suite for `Duplex` / `Connect` / `Serve` adapters in the `webrun-str
|
|
|
10
32
|
- **L3** Mid-stream cancellation — caller `.return()`s output; handler's `finally` runs.
|
|
11
33
|
- **L4** Error propagation — handler `throw`s; caller sees `message` + `stack` + custom fields preserved.
|
|
12
34
|
- **L5** Transport teardown — calling the `serve` teardown twice resolves rather than throwing, and closing the pair after a completed call resolves cleanly. (It does *not* assert what an in-flight call does when the transport closes underneath it; that is deliberately left to each adapter.)
|
|
35
|
+
- **L6** Flow control — a 256 KiB body reaches a deliberately slow consumer intact through a 16 KiB advertised window, so the sender must exhaust its credit and resume on grants sixteen times over. Adapters that accept `mux` options can run it at that window; today that is `-ws` and `webrun-rpc`, the only two with an executable conformance run. `-peerjs` and `-livekit` accept the option but have no pair helper yet, so L6 does not run for them. The loopback, `-webrtc` and `-libp2p` have no `emulateMux` to tune, so for them it is an end-to-end integrity check and nothing more.
|
|
13
36
|
|
|
14
37
|
## Reference loopback
|
|
15
38
|
|
|
@@ -35,6 +58,27 @@ A third argument tunes the suite:
|
|
|
35
58
|
| `concurrency` | `10` | How many concurrent calls L1 runs. |
|
|
36
59
|
| `skipHugeBody` | `false` | Drop L0's 10 MiB case, for transports that rate-limit. |
|
|
37
60
|
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
| Export | Kind | Purpose |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `describeDuplexAdapter(name, makePair, options?)` | function | Registers the whole L0–L6 suite for one adapter. |
|
|
66
|
+
| `makeLoopbackPair()` | `MakePair` | The reference in-process pair; the suite's own self-test. |
|
|
67
|
+
| `MakePair` | type | `(tuning?: PairTuning) => Promise<ConnectServePair>` — called once per test case. |
|
|
68
|
+
| `ConnectServePair` | type | `{ connect(), serve(handler), close() }`. |
|
|
69
|
+
| `PairTuning` | type | `{ mtu?, maxStreamBuffer? }` — flow-control window L6 asks a pair for; an adapter that can forward it to `emulateMux` should. |
|
|
70
|
+
| `DescribeDuplexAdapterOptions` | type | `concurrency` (default 10), `skipHugeBody` (default false). |
|
|
71
|
+
|
|
72
|
+
## Dependencies
|
|
73
|
+
|
|
74
|
+
| Dependency | Kind | Why |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| [`@statewalker/webrun-streams`](../webrun-streams) | runtime | The `Duplex` seam under test. |
|
|
77
|
+
| `vitest` | runtime | The suite is defined with `describe` / `it`. |
|
|
78
|
+
|
|
79
|
+
Consumed as a `devDependency` by every adapter in the family. ESM only
|
|
80
|
+
(`"type": "module"`).
|
|
81
|
+
|
|
38
82
|
## License
|
|
39
83
|
|
|
40
|
-
MIT
|
|
84
|
+
MIT © statewalker — see [LICENSE](../../LICENSE).
|
|
@@ -6,7 +6,7 @@ export interface DescribeDuplexAdapterOptions {
|
|
|
6
6
|
skipHugeBody?: boolean;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Runs every conformance level (L0–
|
|
9
|
+
* Runs every conformance level (L0–L6) against the supplied `ConnectServePair`
|
|
10
10
|
* factory. Each adapter package in the `webrun-streams-*` family invokes this
|
|
11
11
|
* from its own one-line Vitest file.
|
|
12
12
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-duplex-adapter.d.ts","sourceRoot":"","sources":["../src/describe-duplex-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,WAAW,4BAA4B;IAC3C,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,IAAI,GAAE,4BAAiC,GACtC,IAAI,
|
|
1
|
+
{"version":3,"file":"describe-duplex-adapter.d.ts","sourceRoot":"","sources":["../src/describe-duplex-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,WAAW,4BAA4B;IAC3C,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,IAAI,GAAE,4BAAiC,GACtC,IAAI,CAqPN"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { type DescribeDuplexAdapterOptions, describeDuplexAdapter, } from "./describe-duplex-adapter.js";
|
|
2
|
-
export { type ConnectServePair, type MakePair, makeLoopbackPair } from "./loopback.js";
|
|
2
|
+
export { type ConnectServePair, type MakePair, makeLoopbackPair, type PairTuning, } from "./loopback.js";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,GACtB,MAAM,8BAA8B,CAAC;AACtC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,GACtB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,QAAQ,EACb,gBAAgB,EAChB,KAAK,UAAU,GAChB,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { collectBytes } from "@statewalker/webrun-streams";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
//#region src/describe-duplex-adapter.ts
|
|
4
4
|
/**
|
|
5
|
-
* Runs every conformance level (L0–
|
|
5
|
+
* Runs every conformance level (L0–L6) against the supplied `ConnectServePair`
|
|
6
6
|
* factory. Each adapter package in the `webrun-streams-*` family invokes this
|
|
7
7
|
* from its own one-line Vitest file.
|
|
8
8
|
*/
|
|
@@ -194,6 +194,38 @@ function describeDuplexAdapter(name, makePair, opts = {}) {
|
|
|
194
194
|
await expect(pair.close()).resolves.toBeUndefined();
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
|
+
describe("L6: flow control", () => {
|
|
198
|
+
it("delivers a body many times the advertised window to a slow consumer", async () => {
|
|
199
|
+
const pair = await makePair({
|
|
200
|
+
mtu: 4096,
|
|
201
|
+
maxStreamBuffer: 16384
|
|
202
|
+
});
|
|
203
|
+
try {
|
|
204
|
+
await pair.serve(echoHandler);
|
|
205
|
+
const size = 262144;
|
|
206
|
+
const body = new Uint8Array(size);
|
|
207
|
+
for (let i = 0; i < size; i++) body[i] = i & 255;
|
|
208
|
+
const { call, close } = await pair.connect();
|
|
209
|
+
const out = call([body]);
|
|
210
|
+
let received = 0;
|
|
211
|
+
let first = -1;
|
|
212
|
+
let last = -1;
|
|
213
|
+
for await (const chunk of out) {
|
|
214
|
+
if (chunk.byteLength === 0) continue;
|
|
215
|
+
if (first < 0) first = chunk[0];
|
|
216
|
+
last = chunk[chunk.byteLength - 1];
|
|
217
|
+
received += chunk.byteLength;
|
|
218
|
+
await delay(1);
|
|
219
|
+
}
|
|
220
|
+
expect(received).toBe(size);
|
|
221
|
+
expect(first).toBe(0);
|
|
222
|
+
expect(last).toBe(255);
|
|
223
|
+
await close();
|
|
224
|
+
} finally {
|
|
225
|
+
await pair.close();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
197
229
|
});
|
|
198
230
|
}
|
|
199
231
|
const echoHandler = async function* echo(input) {
|
package/dist/loopback.d.ts
CHANGED
|
@@ -11,7 +11,16 @@ export interface ConnectServePair {
|
|
|
11
11
|
serve(handler: Duplex): Promise<() => Promise<void>>;
|
|
12
12
|
close(): Promise<void>;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Flow-control tuning L6 asks a pair for. An adapter that can pass these
|
|
16
|
+
* through to its `emulateMux` should; one that multiplexes natively, or has
|
|
17
|
+
* no configurable window, ignores them and L6 degrades to an integrity check.
|
|
18
|
+
*/
|
|
19
|
+
export interface PairTuning {
|
|
20
|
+
mtu?: number;
|
|
21
|
+
maxStreamBuffer?: number;
|
|
22
|
+
}
|
|
23
|
+
export type MakePair = (tuning?: PairTuning) => Promise<ConnectServePair>;
|
|
15
24
|
/**
|
|
16
25
|
* Loopback pair: `call` invokes the registered `handler` directly, no
|
|
17
26
|
* transport. The conformance suite must pass against this — it self-validates
|
package/dist/loopback.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../src/loopback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC,CAAC;IACjE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../src/loopback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAE1D;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC,CAAC;IACjE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAwC9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-streams-conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Conformance test suite for Duplex/Connect/Serve adapters in the webrun-streams-* family",
|
|
@@ -15,26 +15,32 @@
|
|
|
15
15
|
"url": "git@github.com:statewalker/webrun-wire.git"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
-
".":
|
|
18
|
+
".": {
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
19
23
|
},
|
|
20
24
|
"files": [
|
|
21
25
|
"dist",
|
|
22
26
|
"src"
|
|
23
27
|
],
|
|
24
28
|
"dependencies": {
|
|
25
|
-
"vitest": "^
|
|
26
|
-
"@statewalker/webrun-streams": "0.
|
|
29
|
+
"vitest": "^5.0.3",
|
|
30
|
+
"@statewalker/webrun-streams": "^0.2.1"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
|
-
"@
|
|
33
|
+
"@biomejs/biome": "^2.5.15",
|
|
34
|
+
"@types/node": "^26.6.4",
|
|
30
35
|
"rimraf": "^6.1.3",
|
|
31
|
-
"rolldown": "^1.2.
|
|
36
|
+
"rolldown": "^1.2.12",
|
|
32
37
|
"typescript": "^7.0.2"
|
|
33
38
|
},
|
|
34
39
|
"sideEffects": false,
|
|
35
40
|
"publishConfig": {
|
|
36
41
|
"access": "public"
|
|
37
42
|
},
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
38
44
|
"scripts": {
|
|
39
45
|
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
40
46
|
"test": "vitest run",
|
|
@@ -10,7 +10,7 @@ export interface DescribeDuplexAdapterOptions {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Runs every conformance level (L0–
|
|
13
|
+
* Runs every conformance level (L0–L6) against the supplied `ConnectServePair`
|
|
14
14
|
* factory. Each adapter package in the `webrun-streams-*` family invokes this
|
|
15
15
|
* from its own one-line Vitest file.
|
|
16
16
|
*/
|
|
@@ -212,6 +212,56 @@ export function describeDuplexAdapter(
|
|
|
212
212
|
await expect(pair.close()).resolves.toBeUndefined();
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
|
+
|
|
216
|
+
describe("L6: flow control", () => {
|
|
217
|
+
it("delivers a body many times the advertised window to a slow consumer", async () => {
|
|
218
|
+
// A 16 KiB window against a 256 KiB body: the sender must exhaust its
|
|
219
|
+
// credit and wait for grants sixteen times over. An adapter that
|
|
220
|
+
// ignores the tuning runs this at its own defaults, where it degrades
|
|
221
|
+
// to an integrity check — that is stated in the README, not hidden.
|
|
222
|
+
//
|
|
223
|
+
// What the assertions below check is completion and integrity. That
|
|
224
|
+
// this level also proves *stalling and replenishment* is an
|
|
225
|
+
// elimination argument that lives in the mutations, not in this body:
|
|
226
|
+
// delete the grant `ACK` and replenishment dies, so the transfer hangs
|
|
227
|
+
// and this times out; bypass `reserve()` and stalling dies, so the
|
|
228
|
+
// sender floods the window and the receiver's cap tears the stream
|
|
229
|
+
// down. Both are measured in the plan's Task 5 Step 4 table. Keep the
|
|
230
|
+
// small window and the slow drain — at the adapters' defaults nothing
|
|
231
|
+
// stalls, no grant is emitted, and this level passes with credit
|
|
232
|
+
// deleted entirely. An earlier draft shipped exactly that while
|
|
233
|
+
// claiming otherwise.
|
|
234
|
+
const pair = await makePair({ mtu: 4096, maxStreamBuffer: 16 * 1024 });
|
|
235
|
+
try {
|
|
236
|
+
await pair.serve(echoHandler);
|
|
237
|
+
const size = 256 * 1024;
|
|
238
|
+
const body = new Uint8Array(size);
|
|
239
|
+
for (let i = 0; i < size; i++) body[i] = i & 0xff;
|
|
240
|
+
|
|
241
|
+
const { call, close } = await pair.connect();
|
|
242
|
+
const out = call([body]);
|
|
243
|
+
let received = 0;
|
|
244
|
+
let first = -1;
|
|
245
|
+
let last = -1;
|
|
246
|
+
for await (const chunk of out) {
|
|
247
|
+
if (chunk.byteLength === 0) continue;
|
|
248
|
+
if (first < 0) first = chunk[0] as number;
|
|
249
|
+
last = chunk[chunk.byteLength - 1] as number;
|
|
250
|
+
received += chunk.byteLength;
|
|
251
|
+
// Drain deliberately slowly, so the sender is stalled on credit
|
|
252
|
+
// for most of the transfer rather than streaming through.
|
|
253
|
+
await delay(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
expect(received).toBe(size);
|
|
257
|
+
expect(first).toBe(0);
|
|
258
|
+
expect(last).toBe((size - 1) & 0xff);
|
|
259
|
+
await close();
|
|
260
|
+
} finally {
|
|
261
|
+
await pair.close();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
215
265
|
});
|
|
216
266
|
}
|
|
217
267
|
|
package/src/index.ts
CHANGED
|
@@ -2,4 +2,9 @@ export {
|
|
|
2
2
|
type DescribeDuplexAdapterOptions,
|
|
3
3
|
describeDuplexAdapter,
|
|
4
4
|
} from "./describe-duplex-adapter.js";
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
type ConnectServePair,
|
|
7
|
+
type MakePair,
|
|
8
|
+
makeLoopbackPair,
|
|
9
|
+
type PairTuning,
|
|
10
|
+
} from "./loopback.js";
|
package/src/loopback.ts
CHANGED
|
@@ -10,7 +10,17 @@ export interface ConnectServePair {
|
|
|
10
10
|
close(): Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Flow-control tuning L6 asks a pair for. An adapter that can pass these
|
|
15
|
+
* through to its `emulateMux` should; one that multiplexes natively, or has
|
|
16
|
+
* no configurable window, ignores them and L6 degrades to an integrity check.
|
|
17
|
+
*/
|
|
18
|
+
export interface PairTuning {
|
|
19
|
+
mtu?: number;
|
|
20
|
+
maxStreamBuffer?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type MakePair = (tuning?: PairTuning) => Promise<ConnectServePair>;
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* Loopback pair: `call` invokes the registered `handler` directly, no
|