fastapi-streams 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/CHANGELOG.md +35 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/PKG-INFO +1 -1
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/clients.md +9 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/codegen.md +20 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/development.md +29 -0
- {fastapi_streams-0.3.0/fastapi_streams/codegen/templates/python → fastapi_streams-0.3.1/examples/agent_runs/generated_python}/runtime.py +29 -31
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/__init__.py +1 -1
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/cli.py +65 -6
- {fastapi_streams-0.3.0/examples/agent_runs/generated_python → fastapi_streams-0.3.1/fastapi_streams/codegen/templates/python}/runtime.py +29 -31
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/pyproject.toml +1 -1
- fastapi_streams-0.3.1/scripts/benchmark_runtime.mjs +93 -0
- fastapi_streams-0.3.1/scripts/benchmark_runtime.py +293 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/scripts/smoke_package.py +18 -7
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_structured_diagnostics.py +67 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_unknown_events.py +50 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/runtime.py +29 -31
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/.gitattributes +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/.github/workflows/ci.yml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/.github/workflows/compatibility.yml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/.github/workflows/release.yml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/.gitignore +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/.pre-commit-config.yaml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/AGENTS.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/LICENSE +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/architecture.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/native-transports.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/quickstart.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/docs/releasing.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/consume.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/consume.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/errors.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/.fastapi-streams.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/types.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/.fastapi-streams.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/agent_runs/api.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/client.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/models.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/py.typed +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/native_proxy.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/examples/rest_codegen.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/diagnostics.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/documentation.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/enums.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/ir.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/names.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/openapi.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/python.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/schemas/LICENSE +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/schemas/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/python/api.py.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/sse.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templating.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/typescript.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/writer.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/py.typed +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/fastapi_streams/router.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/package-lock.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/package.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/scripts/check_package.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/scripts/check_release.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/scripts/codegen.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/scripts/test_compat.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/helpers.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_additional_properties.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_codegen.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_contract_boundaries.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_diagnostics.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_discriminators.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_documentation.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_error_contracts.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_field_names.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_http_backends.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_intersections.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_literal_fields.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_native_contract.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_nested_models.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_python_client.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_python_typing.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_request_validation.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_required_properties.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_scalar_constraints.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_schema_matrix.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/codegen/test_stream_validation_details.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/contracts/test_openapi.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/agent_runs.openapi.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/agent_runs.typescript.diagnostics.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/compat-client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/.fastapi-streams.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/fixtures/api.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/fixtures/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/types.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/.fastapi-streams.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/client.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/fixtures/api.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/models.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/py.typed +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/integration_app.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/models.openapi.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/fixtures/models_app.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/integration/test_end_to_end.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/router/test_router.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/test_release.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typescript/errors.test.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typescript/live-client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typescript/models.test.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typescript/narrowing.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typescript/sse.test.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typescript/tsconfig.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.3.1}/tests/typing/client_usage.py +0 -0
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
All notable changes and consumer upgrade steps are recorded here. Entries marked
|
|
4
4
|
unreleased describe prepared source versions that have not been published.
|
|
5
5
|
|
|
6
|
+
## 0.3.1 - 2026-10-18
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Repeatable Python and Node benchmarks for generated SDK request preparation,
|
|
11
|
+
decoding/validation, SSE framing, complete consumption, fragmented chunks and
|
|
12
|
+
Python unknown-event ignoring. Reports include individual timing samples and
|
|
13
|
+
optional separate Python peak-memory measurements; no CI timing thresholds.
|
|
14
|
+
- Add generation provenance and validation capabilities to version 1 diagnostic
|
|
15
|
+
reports: canonical schema SHA-256, generator version, target/HTTP backend and
|
|
16
|
+
request/event/error validation phases. Existing diagnostic fields, IDs and
|
|
17
|
+
exact reviewed allowances remain unchanged. Additional report object keys are
|
|
18
|
+
additive; the metadata does not introduce runtime checks or full JSON Schema
|
|
19
|
+
guarantees.
|
|
20
|
+
|
|
21
|
+
### Performance
|
|
22
|
+
|
|
23
|
+
- Reuse the JSON-value adapter for skipped unknown events and HTTP-error JSON
|
|
24
|
+
decoding. Cache top-level discriminator metadata per generated event adapter
|
|
25
|
+
instead of rebuilding its JSON Schema for every ignore-policy stream.
|
|
26
|
+
- Prepare sanitized validation details only for errors that are actually raised.
|
|
27
|
+
Known events still use direct Pydantic JSON validation; malformed known events,
|
|
28
|
+
missing/non-string tags, invalid JSON and unknown nested variants still fail.
|
|
29
|
+
Public signatures, coercion, validation timing and wire encoding are unchanged.
|
|
30
|
+
|
|
31
|
+
### Upgrade from 0.3.0
|
|
32
|
+
|
|
33
|
+
1. Upgrade the generator to 0.3.1 and regenerate with the existing HTTP backend
|
|
34
|
+
to obtain the Python runtime improvements. Existing generated SDKs keep working.
|
|
35
|
+
2. Diagnostic-report readers should tolerate the new `generation` and `validation`
|
|
36
|
+
keys. Existing exact diagnostic allowances need no migration.
|
|
37
|
+
3. Continue validating server requests and domain invariants at their own
|
|
38
|
+
boundaries; consumers should reuse parsed events instead of validating the
|
|
39
|
+
same schema again. TypeScript remains static-only with no added dependency.
|
|
40
|
+
|
|
6
41
|
## 0.3.0 - 2026-10-18
|
|
7
42
|
|
|
8
43
|
### Breaking changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-streams
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Typed FastAPI streams with generated TypeScript and Python clients.
|
|
5
5
|
Project-URL: Documentation, https://github.com/mathisarends/canon/tree/main/packages/fastapi-streams/docs
|
|
6
6
|
Project-URL: Source, https://github.com/mathisarends/canon/tree/main/packages/fastapi-streams
|
|
@@ -121,6 +121,15 @@ remain ordinary members of the user-defined union.
|
|
|
121
121
|
|
|
122
122
|
## HTTP error contracts
|
|
123
123
|
|
|
124
|
+
Keep schema validation at transport boundaries. Python prepares and validates
|
|
125
|
+
requests at the method call and validates received events during iteration.
|
|
126
|
+
Pass those parsed event models onward instead of serializing them to dictionaries
|
|
127
|
+
and validating the same schema again. Server request validation remains necessary
|
|
128
|
+
regardless of SDK checks; authentication, authorization and domain invariants have
|
|
129
|
+
separate responsibilities. Canon composition does not need an additional event
|
|
130
|
+
validator around Streams. TypeScript keeps its documented static-only behavior;
|
|
131
|
+
applications needing runtime event checks can validate once before using a value.
|
|
132
|
+
|
|
124
133
|
Python request parameters and JSON bodies are validated when calling the method;
|
|
125
134
|
catch Pydantic `ValidationError` around that call. Network and event errors occur
|
|
126
135
|
while iterating. See the [validation matrix and CI policy](codegen.md#validation-support-matrix).
|
|
@@ -191,6 +191,26 @@ including inline schemas and referenced component definitions. Shared definition
|
|
|
191
191
|
identify each affected operation; unused components do not generate warnings.
|
|
192
192
|
SSE framing metadata is outside generated event-model validation.
|
|
193
193
|
|
|
194
|
+
The version 1 report also carries additive `generation` and `validation` objects.
|
|
195
|
+
Consumers should tolerate additional object keys. `generation` records the
|
|
196
|
+
generator name/version, target, selected Python HTTP library (null for TypeScript)
|
|
197
|
+
and `schema_sha256`. The hash covers the complete parsed OpenAPI document,
|
|
198
|
+
including unused definitions. Its `canonical-json-v1` encoding is Python
|
|
199
|
+
`json.dumps(document, sort_keys=True, separators=(",", ":"), ensure_ascii=False)`
|
|
200
|
+
encoded as UTF-8. Whitespace and object-key order in the input file do not affect
|
|
201
|
+
the hash; array order and values do. It is a content identifier, not a signature
|
|
202
|
+
or proof of schema enforcement.
|
|
203
|
+
|
|
204
|
+
`validation` distinguishes parameter/body validation at `method_call`, event
|
|
205
|
+
decoding at `iteration` and HTTP-error handling at `consumer_controlled`.
|
|
206
|
+
TypeScript parameter/body schema checks are `not_performed`. Each boundary has
|
|
207
|
+
a `schema_validation` boolean, with `json_decoding: true` for events. Python's
|
|
208
|
+
`runtime_scope` is `generated_types_and_supported_constraints`; TypeScript's is
|
|
209
|
+
`json_decoding_only`. Both set `full_json_schema: false`. These target capabilities
|
|
210
|
+
describe supported boundaries, even if an operation has no parameters or body;
|
|
211
|
+
source-specific limitations remain in `diagnostics` with their accepted status.
|
|
212
|
+
The report is generated only when requested and adds no SDK runtime checks.
|
|
213
|
+
|
|
194
214
|
`unsupported-constraint` identifies an unenforced rule for the selected target;
|
|
195
215
|
TypeScript also diagnoses Python-supported scalar constraints because TypeScript
|
|
196
216
|
has no runtime schema validator. Python's `optional-discriminator` diagnoses event
|
|
@@ -88,6 +88,35 @@ outside the checkout they invoke the installed `fastapi-streams` command, exerci
|
|
|
88
88
|
and consume an event through the generated Python SDK. Metadata and `__version__`
|
|
89
89
|
must match the release version in `pyproject.toml`.
|
|
90
90
|
|
|
91
|
+
## Generated SDK performance
|
|
92
|
+
|
|
93
|
+
From the workspace root, use the locked environment and installed Node tooling:
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
uv run --locked python packages/fastapi-streams/scripts/benchmark_runtime.py --memory --output dist/streams-baseline.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The harness generates both SDKs from the same small discriminated-union contract.
|
|
100
|
+
It measures request preparation separately from decoding, framing and complete
|
|
101
|
+
HTTP/SSE consumption with an in-memory HTTPX transport or Fetch response. Cases
|
|
102
|
+
cover small token events, nested batches, seven-byte fragments and Python's
|
|
103
|
+
unknown-event ignore policy. Python validates generated models; TypeScript only
|
|
104
|
+
decodes JSON. Their decoding results do not represent equivalent guarantees.
|
|
105
|
+
|
|
106
|
+
Reports record source location, generator/runtime versions, platform, payload
|
|
107
|
+
sizes, individual samples, median CPU/wall time per request or input event, and
|
|
108
|
+
throughput. Ignore-policy throughput counts skipped input events too. `--memory`
|
|
109
|
+
adds a separate Python traced peak-memory sample; it does not instrument timings
|
|
110
|
+
or count total allocations. There are three warmup iterations per case. These
|
|
111
|
+
are warmed batch measurements, not individual-event latency percentiles, cold
|
|
112
|
+
startup measurements, real-network results or Hermes deployment evidence.
|
|
113
|
+
|
|
114
|
+
Use `--runtime python` or `--runtime typescript` for focused runs. Adjust
|
|
115
|
+
`--events`, `--iterations` and `--rounds` for longer samples. For a comparison,
|
|
116
|
+
run the same harness with `--source-dir` pointing to the other revision's Streams
|
|
117
|
+
package directory, keeping the interpreter, dependencies and arguments fixed.
|
|
118
|
+
Repeat runs to assess noise; ordinary CI does not enforce timing thresholds.
|
|
119
|
+
|
|
91
120
|
## Supported scope and roadmap
|
|
92
121
|
|
|
93
122
|
GET/POST JSON SSE data; scalar path and header parameters; scalar/array query parameters;
|
|
@@ -5,12 +5,15 @@ import codecs
|
|
|
5
5
|
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from functools import cache
|
|
8
9
|
from typing import Literal, cast
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
from pydantic import JsonValue, TypeAdapter, ValidationError
|
|
12
13
|
from pydantic_core import ErrorDetails
|
|
13
14
|
|
|
15
|
+
_JSON_ADAPTER = TypeAdapter[JsonValue](JsonValue)
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
class TransportError(Exception):
|
|
16
19
|
pass
|
|
@@ -180,19 +183,11 @@ class Transport:
|
|
|
180
183
|
options: RequestOptions | None = None,
|
|
181
184
|
) -> AsyncGenerator[T, None]:
|
|
182
185
|
discriminator: str | None = None
|
|
183
|
-
known_tags:
|
|
186
|
+
known_tags: frozenset[str] = frozenset()
|
|
184
187
|
if options and options.unknown_events not in {"error", "ignore"}:
|
|
185
188
|
raise ValueError("unknown_events must be 'error' or 'ignore'")
|
|
186
189
|
if options and options.unknown_events == "ignore":
|
|
187
|
-
|
|
188
|
-
policy = schema.get("discriminator", {})
|
|
189
|
-
discriminator = policy.get("propertyName")
|
|
190
|
-
known_tags = set(policy.get("mapping", {}))
|
|
191
|
-
if not isinstance(discriminator, str) or not known_tags:
|
|
192
|
-
raise ValueError(
|
|
193
|
-
"unknown_events='ignore' requires a top-level "
|
|
194
|
-
"discriminated event union"
|
|
195
|
-
)
|
|
190
|
+
discriminator, known_tags = _unknown_event_policy(adapter)
|
|
196
191
|
request_headers = self.headers.copy()
|
|
197
192
|
request_headers.update(headers)
|
|
198
193
|
if options and options.headers:
|
|
@@ -223,11 +218,7 @@ class Transport:
|
|
|
223
218
|
failure = exc
|
|
224
219
|
text = raw.decode("utf-8", errors="replace")
|
|
225
220
|
try:
|
|
226
|
-
details = (
|
|
227
|
-
TypeAdapter[JsonValue](JsonValue).validate_json(text)
|
|
228
|
-
if text
|
|
229
|
-
else None
|
|
230
|
-
)
|
|
221
|
+
details = _JSON_ADAPTER.validate_json(text) if text else None
|
|
231
222
|
except ValidationError:
|
|
232
223
|
details = text
|
|
233
224
|
raise HttpError(response, details) from failure
|
|
@@ -245,6 +236,16 @@ class Transport:
|
|
|
245
236
|
try:
|
|
246
237
|
yield adapter.validate_json(payload)
|
|
247
238
|
except ValidationError as exc:
|
|
239
|
+
if discriminator is not None:
|
|
240
|
+
try:
|
|
241
|
+
value = _JSON_ADAPTER.validate_json(payload)
|
|
242
|
+
except ValidationError:
|
|
243
|
+
pass
|
|
244
|
+
else:
|
|
245
|
+
if isinstance(value, dict):
|
|
246
|
+
tag = value.get(discriminator)
|
|
247
|
+
if isinstance(tag, str) and tag not in known_tags:
|
|
248
|
+
continue
|
|
248
249
|
# Exclude input and arbitrary validator context from public
|
|
249
250
|
# diagnostics. The original cause remains available explicitly.
|
|
250
251
|
details = exc.errors(
|
|
@@ -257,22 +258,6 @@ class Transport:
|
|
|
257
258
|
if any(error["type"] == "json_invalid" for error in details)
|
|
258
259
|
else "invalid_event_schema"
|
|
259
260
|
)
|
|
260
|
-
if discriminator is not None:
|
|
261
|
-
try:
|
|
262
|
-
value = TypeAdapter[JsonValue](JsonValue).validate_json(
|
|
263
|
-
payload
|
|
264
|
-
)
|
|
265
|
-
except ValidationError:
|
|
266
|
-
raise StreamParseError(
|
|
267
|
-
payload,
|
|
268
|
-
operation_id=operation_id,
|
|
269
|
-
category="invalid_json",
|
|
270
|
-
validation_errors=details,
|
|
271
|
-
) from exc
|
|
272
|
-
if isinstance(value, dict):
|
|
273
|
-
tag = value.get(discriminator)
|
|
274
|
-
if isinstance(tag, str) and tag not in known_tags:
|
|
275
|
-
continue
|
|
276
261
|
raise StreamParseError(
|
|
277
262
|
payload,
|
|
278
263
|
operation_id=operation_id,
|
|
@@ -285,6 +270,19 @@ class Transport:
|
|
|
285
270
|
raise TransportError("Invalid UTF-8 in SSE stream") from exc
|
|
286
271
|
|
|
287
272
|
|
|
273
|
+
@cache
|
|
274
|
+
def _unknown_event_policy[T](adapter: TypeAdapter[T]) -> tuple[str, frozenset[str]]:
|
|
275
|
+
schema = adapter.json_schema()
|
|
276
|
+
policy = schema.get("discriminator", {})
|
|
277
|
+
discriminator = policy.get("propertyName")
|
|
278
|
+
known_tags = frozenset(policy.get("mapping", {}))
|
|
279
|
+
if not isinstance(discriminator, str) or not known_tags:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"unknown_events='ignore' requires a top-level discriminated event union"
|
|
282
|
+
)
|
|
283
|
+
return discriminator, known_tags
|
|
284
|
+
|
|
285
|
+
|
|
288
286
|
def _preserve_numbers[T](original: object, validated: T) -> T:
|
|
289
287
|
if (
|
|
290
288
|
type(original) is int
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import hashlib
|
|
4
5
|
import importlib
|
|
5
6
|
import json
|
|
6
7
|
import sys
|
|
@@ -9,6 +10,7 @@ from typing import TYPE_CHECKING, cast
|
|
|
9
10
|
|
|
10
11
|
from fastapi import FastAPI
|
|
11
12
|
|
|
13
|
+
from fastapi_streams import __version__
|
|
12
14
|
from fastapi_streams.codegen.openapi import ContractError, Document, export_schema
|
|
13
15
|
from fastapi_streams.codegen.writer import write_files
|
|
14
16
|
|
|
@@ -103,6 +105,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
103
105
|
document = json.loads(args.schema.read_text(encoding="utf-8"))
|
|
104
106
|
if not isinstance(document, dict):
|
|
105
107
|
raise ContractError("The OpenAPI document must be an object")
|
|
108
|
+
document = cast(Document, document)
|
|
106
109
|
if args.command == "check" and args.app:
|
|
107
110
|
expected = export_schema(_load_app(args.app))
|
|
108
111
|
if expected != document:
|
|
@@ -112,12 +115,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
112
115
|
)
|
|
113
116
|
return 1
|
|
114
117
|
diagnostics: list[GenerationDiagnostic] = []
|
|
115
|
-
contract = build_ir(
|
|
116
|
-
cast(Document, document), target=args.language, diagnostics=diagnostics
|
|
117
|
-
)
|
|
118
|
+
contract = build_ir(document, target=args.language, diagnostics=diagnostics)
|
|
118
119
|
# Reports precede strict-policy failures, so CI can review new IDs.
|
|
119
120
|
if args.diagnostics:
|
|
120
|
-
_write_diagnostics(
|
|
121
|
+
_write_diagnostics(
|
|
122
|
+
args.diagnostics,
|
|
123
|
+
diagnostics,
|
|
124
|
+
set(),
|
|
125
|
+
document,
|
|
126
|
+
args.language,
|
|
127
|
+
args.python_http_client or "httpx",
|
|
128
|
+
)
|
|
121
129
|
accepted: set[str] = (
|
|
122
130
|
accepted_diagnostics(
|
|
123
131
|
diagnostics, json.loads(args.allow_diagnostics.read_text("utf-8"))
|
|
@@ -126,7 +134,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
126
134
|
else set()
|
|
127
135
|
)
|
|
128
136
|
if args.diagnostics:
|
|
129
|
-
_write_diagnostics(
|
|
137
|
+
_write_diagnostics(
|
|
138
|
+
args.diagnostics,
|
|
139
|
+
diagnostics,
|
|
140
|
+
accepted,
|
|
141
|
+
document,
|
|
142
|
+
args.language,
|
|
143
|
+
args.python_http_client or "httpx",
|
|
144
|
+
)
|
|
130
145
|
pending = [item for item in diagnostics if item.id not in accepted]
|
|
131
146
|
emit_diagnostics(pending)
|
|
132
147
|
if args.strict and pending:
|
|
@@ -168,13 +183,57 @@ def _load_app(value: str) -> FastAPI:
|
|
|
168
183
|
|
|
169
184
|
|
|
170
185
|
def _write_diagnostics(
|
|
171
|
-
path: Path,
|
|
186
|
+
path: Path,
|
|
187
|
+
diagnostics: list[GenerationDiagnostic],
|
|
188
|
+
accepted: set[str],
|
|
189
|
+
document: Document,
|
|
190
|
+
target: str,
|
|
191
|
+
http_client: str,
|
|
172
192
|
) -> None:
|
|
193
|
+
# Canonical JSON binds reports to schema content, independent of file layout.
|
|
194
|
+
encoded = json.dumps(
|
|
195
|
+
document, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
196
|
+
).encode("utf-8")
|
|
197
|
+
python = target == "python"
|
|
198
|
+
request_phase = "method_call" if python else "not_performed"
|
|
173
199
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
200
|
path.write_text(
|
|
175
201
|
json.dumps(
|
|
176
202
|
{
|
|
177
203
|
"version": 1,
|
|
204
|
+
"generation": {
|
|
205
|
+
"generator": "fastapi-streams",
|
|
206
|
+
"generator_version": __version__,
|
|
207
|
+
"schema_sha256": hashlib.sha256(encoded).hexdigest(),
|
|
208
|
+
"schema_hash_encoding": "canonical-json-v1",
|
|
209
|
+
"target": target,
|
|
210
|
+
"python_http_client": http_client if python else None,
|
|
211
|
+
},
|
|
212
|
+
"validation": {
|
|
213
|
+
"full_json_schema": False,
|
|
214
|
+
"runtime_scope": (
|
|
215
|
+
"generated_types_and_supported_constraints"
|
|
216
|
+
if python
|
|
217
|
+
else "json_decoding_only"
|
|
218
|
+
),
|
|
219
|
+
"parameters": {
|
|
220
|
+
"phase": request_phase,
|
|
221
|
+
"schema_validation": python,
|
|
222
|
+
},
|
|
223
|
+
"request_body": {
|
|
224
|
+
"phase": request_phase,
|
|
225
|
+
"schema_validation": python,
|
|
226
|
+
},
|
|
227
|
+
"events": {
|
|
228
|
+
"phase": "iteration",
|
|
229
|
+
"schema_validation": python,
|
|
230
|
+
"json_decoding": True,
|
|
231
|
+
},
|
|
232
|
+
"http_error_bodies": {
|
|
233
|
+
"phase": "consumer_controlled",
|
|
234
|
+
"schema_validation": False,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
178
237
|
"diagnostics": [
|
|
179
238
|
item.to_dict(accepted=item.id in accepted) for item in diagnostics
|
|
180
239
|
],
|
|
@@ -5,12 +5,15 @@ import codecs
|
|
|
5
5
|
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from functools import cache
|
|
8
9
|
from typing import Literal, cast
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
from pydantic import JsonValue, TypeAdapter, ValidationError
|
|
12
13
|
from pydantic_core import ErrorDetails
|
|
13
14
|
|
|
15
|
+
_JSON_ADAPTER = TypeAdapter[JsonValue](JsonValue)
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
class TransportError(Exception):
|
|
16
19
|
pass
|
|
@@ -180,19 +183,11 @@ class Transport:
|
|
|
180
183
|
options: RequestOptions | None = None,
|
|
181
184
|
) -> AsyncGenerator[T, None]:
|
|
182
185
|
discriminator: str | None = None
|
|
183
|
-
known_tags:
|
|
186
|
+
known_tags: frozenset[str] = frozenset()
|
|
184
187
|
if options and options.unknown_events not in {"error", "ignore"}:
|
|
185
188
|
raise ValueError("unknown_events must be 'error' or 'ignore'")
|
|
186
189
|
if options and options.unknown_events == "ignore":
|
|
187
|
-
|
|
188
|
-
policy = schema.get("discriminator", {})
|
|
189
|
-
discriminator = policy.get("propertyName")
|
|
190
|
-
known_tags = set(policy.get("mapping", {}))
|
|
191
|
-
if not isinstance(discriminator, str) or not known_tags:
|
|
192
|
-
raise ValueError(
|
|
193
|
-
"unknown_events='ignore' requires a top-level "
|
|
194
|
-
"discriminated event union"
|
|
195
|
-
)
|
|
190
|
+
discriminator, known_tags = _unknown_event_policy(adapter)
|
|
196
191
|
request_headers = self.headers.copy()
|
|
197
192
|
request_headers.update(headers)
|
|
198
193
|
if options and options.headers:
|
|
@@ -223,11 +218,7 @@ class Transport:
|
|
|
223
218
|
failure = exc
|
|
224
219
|
text = raw.decode("utf-8", errors="replace")
|
|
225
220
|
try:
|
|
226
|
-
details = (
|
|
227
|
-
TypeAdapter[JsonValue](JsonValue).validate_json(text)
|
|
228
|
-
if text
|
|
229
|
-
else None
|
|
230
|
-
)
|
|
221
|
+
details = _JSON_ADAPTER.validate_json(text) if text else None
|
|
231
222
|
except ValidationError:
|
|
232
223
|
details = text
|
|
233
224
|
raise HttpError(response, details) from failure
|
|
@@ -245,6 +236,16 @@ class Transport:
|
|
|
245
236
|
try:
|
|
246
237
|
yield adapter.validate_json(payload)
|
|
247
238
|
except ValidationError as exc:
|
|
239
|
+
if discriminator is not None:
|
|
240
|
+
try:
|
|
241
|
+
value = _JSON_ADAPTER.validate_json(payload)
|
|
242
|
+
except ValidationError:
|
|
243
|
+
pass
|
|
244
|
+
else:
|
|
245
|
+
if isinstance(value, dict):
|
|
246
|
+
tag = value.get(discriminator)
|
|
247
|
+
if isinstance(tag, str) and tag not in known_tags:
|
|
248
|
+
continue
|
|
248
249
|
# Exclude input and arbitrary validator context from public
|
|
249
250
|
# diagnostics. The original cause remains available explicitly.
|
|
250
251
|
details = exc.errors(
|
|
@@ -257,22 +258,6 @@ class Transport:
|
|
|
257
258
|
if any(error["type"] == "json_invalid" for error in details)
|
|
258
259
|
else "invalid_event_schema"
|
|
259
260
|
)
|
|
260
|
-
if discriminator is not None:
|
|
261
|
-
try:
|
|
262
|
-
value = TypeAdapter[JsonValue](JsonValue).validate_json(
|
|
263
|
-
payload
|
|
264
|
-
)
|
|
265
|
-
except ValidationError:
|
|
266
|
-
raise StreamParseError(
|
|
267
|
-
payload,
|
|
268
|
-
operation_id=operation_id,
|
|
269
|
-
category="invalid_json",
|
|
270
|
-
validation_errors=details,
|
|
271
|
-
) from exc
|
|
272
|
-
if isinstance(value, dict):
|
|
273
|
-
tag = value.get(discriminator)
|
|
274
|
-
if isinstance(tag, str) and tag not in known_tags:
|
|
275
|
-
continue
|
|
276
261
|
raise StreamParseError(
|
|
277
262
|
payload,
|
|
278
263
|
operation_id=operation_id,
|
|
@@ -285,6 +270,19 @@ class Transport:
|
|
|
285
270
|
raise TransportError("Invalid UTF-8 in SSE stream") from exc
|
|
286
271
|
|
|
287
272
|
|
|
273
|
+
@cache
|
|
274
|
+
def _unknown_event_policy[T](adapter: TypeAdapter[T]) -> tuple[str, frozenset[str]]:
|
|
275
|
+
schema = adapter.json_schema()
|
|
276
|
+
policy = schema.get("discriminator", {})
|
|
277
|
+
discriminator = policy.get("propertyName")
|
|
278
|
+
known_tags = frozenset(policy.get("mapping", {}))
|
|
279
|
+
if not isinstance(discriminator, str) or not known_tags:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"unknown_events='ignore' requires a top-level discriminated event union"
|
|
282
|
+
)
|
|
283
|
+
return discriminator, known_tags
|
|
284
|
+
|
|
285
|
+
|
|
288
286
|
def _preserve_numbers[T](original: object, validated: T) -> T:
|
|
289
287
|
if (
|
|
290
288
|
type(original) is int
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Invoked by benchmark_runtime.py against a freshly generated, compiled SDK.
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const [sdk, payloadPath, eventArg, iterationArg, roundArg] = process.argv.slice(2);
|
|
7
|
+
const events = Number(eventArg);
|
|
8
|
+
const iterations = Number(iterationArg);
|
|
9
|
+
const rounds = Number(roundArg);
|
|
10
|
+
const { createClient } = await import(pathToFileURL(resolve(sdk, "index.js")).href);
|
|
11
|
+
const { parseSSE } = await import(pathToFileURL(resolve(sdk, "transport/sse.js")).href);
|
|
12
|
+
const payloads = JSON.parse(await readFile(payloadPath, "utf8"));
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
let activeChunks = [];
|
|
15
|
+
let sink;
|
|
16
|
+
|
|
17
|
+
function body() {
|
|
18
|
+
return new ReadableStream({
|
|
19
|
+
start(controller) {
|
|
20
|
+
for (const chunk of activeChunks) controller.enqueue(chunk);
|
|
21
|
+
controller.close();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const client = createClient({
|
|
27
|
+
baseUrl: "http://benchmark.test",
|
|
28
|
+
fetch: async () => new Response(body(), { headers: { "content-type": "text/event-stream" } }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function measure(operation, repeats, units) {
|
|
32
|
+
for (let i = 0; i < 3; i++) await operation();
|
|
33
|
+
const wall = [];
|
|
34
|
+
const cpu = [];
|
|
35
|
+
for (let round = 0; round < rounds; round++) {
|
|
36
|
+
const started = process.hrtime.bigint();
|
|
37
|
+
const cpuStarted = process.cpuUsage();
|
|
38
|
+
for (let i = 0; i < repeats; i++) await operation();
|
|
39
|
+
const elapsedCpu = process.cpuUsage(cpuStarted);
|
|
40
|
+
cpu.push((elapsedCpu.user + elapsedCpu.system) / repeats / units);
|
|
41
|
+
wall.push(Number(process.hrtime.bigint() - started) / repeats / units / 1000);
|
|
42
|
+
}
|
|
43
|
+
const median = values => {
|
|
44
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
45
|
+
const middle = Math.floor(sorted.length / 2);
|
|
46
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
iterations_per_round: repeats,
|
|
50
|
+
units_per_iteration: units,
|
|
51
|
+
median_us_per_unit: median(wall),
|
|
52
|
+
cpu_median_us_per_unit: median(cpu),
|
|
53
|
+
units_per_second: 1_000_000 / median(wall),
|
|
54
|
+
samples_us_per_unit: wall,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = {};
|
|
59
|
+
results.request_preparation = await measure(async () => {
|
|
60
|
+
sink = client.bench.events({ runId: "run_abc", limit: 100 });
|
|
61
|
+
}, iterations * events, 1);
|
|
62
|
+
|
|
63
|
+
for (const name of ["small", "nested"]) {
|
|
64
|
+
results[`decode_static_${name}`] = await measure(async () => {
|
|
65
|
+
sink = JSON.parse(payloads[name]);
|
|
66
|
+
}, iterations * events, 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const name of ["small", "nested", "fragmented"]) {
|
|
70
|
+
const payload = payloads[name === "nested" ? "nested" : "small"];
|
|
71
|
+
const raw = encoder.encode(`data: ${payload}\n\n`.repeat(events));
|
|
72
|
+
activeChunks = name === "fragmented"
|
|
73
|
+
? Array.from({ length: Math.ceil(raw.length / 7) }, (_, index) => raw.subarray(index * 7, index * 7 + 7))
|
|
74
|
+
: [raw];
|
|
75
|
+
results[`stream_${name}`] = await measure(async () => {
|
|
76
|
+
let received = 0;
|
|
77
|
+
for await (const event of client.bench.events({ runId: "run_abc" })) {
|
|
78
|
+
sink = event;
|
|
79
|
+
received++;
|
|
80
|
+
}
|
|
81
|
+
if (received !== events) throw new Error("Incomplete benchmark stream");
|
|
82
|
+
}, iterations, events);
|
|
83
|
+
results[`frame_decode_${name}`] = await measure(async () => {
|
|
84
|
+
for await (const event of parseSSE(body())) sink = event;
|
|
85
|
+
}, iterations, events);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (sink === undefined) throw new Error("Benchmark did not execute");
|
|
89
|
+
process.stdout.write(JSON.stringify({
|
|
90
|
+
version: process.version,
|
|
91
|
+
event_validation: "JSON decoding and static types; no runtime schema validation",
|
|
92
|
+
results,
|
|
93
|
+
}));
|