feat(core): add the HTTP domain model — Request, Response, Headers, Status, MediaType, Protocol, QueryParams, RequestOptions, ETag, HttpRange, RequestConditions. - #28
Conversation
…tatus, MediaType, Protocol, QueryParams, RequestOptions, ETag, HttpRange, RequestConditions
Known gapsFull detail in
|
Decisions to agree/disagree with:Classes over plain frozen objects. Styleguide 6.3 defaults to Outbound and inbound header validation are separate paths, not a flag. Query encode and parse are deliberately asymmetric. Error messages never leak. One predicate, not two. HTTP-26 requires media-type construction to reject forbidden bytes "using the same |
Phase 1 — Core HTTP Domain Model
Implements the immutable, transport-agnostic HTTP domain model in
@dexpace/core: the first real domain codein the repository, and the first time the Phase 0 toolchain gates run against something other than a stub.
Governed by
docs/product-spec/04-core-http-domain-model.md(normative), mapped to TypeScript perdocs/sdk-design-nodejs/04-domain-model-construction.md, planned indocs/superpowers/plans/2026-07-23-phase1-core-http-domain-model.md.Scope
Implemented: HTTP-3, 4, 5 (construction, immutability, derivation) · HTTP-6, 7, 8, 9 (request and method
legality) · HTTP-10, 11, 12 (status) · HTTP-13 – 22 (headers) · HTTP-23 – 27, 53 (media type) · HTTP-28 – 32
(query params) · HTTP-33 (protocol) · HTTP-34, 35 (request options) · HTTP-46, 47 (URL equality and
construction) · HTTP-48, 49, 50 (conditional-request helpers) · SEAM-29 (shared
Builder<T>contract).Verified, not newly implemented: SEAM-1. Phase 0 already satisfied it with an empty
dependenciesfield;this phase's job was to not break it while adding real logic, which
verify:seam-1confirms.Deliberately deferred to Phase 3b: HTTP-36 – 45 (body lifecycle,
TypedResponse<T>), HTTP-51(
MultipartBody), HTTP-52 (error-body cap), and HTTP-46's body-by-value equality clause. All depend on bodylifecycle contracts that Phase 3b owns.
Request/Responsecarrybody: unknownas an explicit placeholder —this phase only needs presence-or-absence to enforce HTTP-7/8.
Note: the design doc's scope line still reads "HTTP-3 through HTTP-53" and needs correcting to match the
plan's own amendment (tracked as C1 in
docs/open-items.md).Architecture
Every model follows one construction shape. It is worth understanding once, because thirteen files repeat it
and none of it is enforced by tooling:
#privatefields, not TSprivate. Styleguide 6.7 carves this out for libraries whose internals muststay unreachable even reflectively.
privateconstructor, so no public field-wise constructor reaches the emitted.d.tsand a consumercannot construct around
build()'s validation (HTTP-2).createXfriend-class hook. TypeScript has no friend classes, so each builder reaches its model'sprivate constructor through a module-scoped
let createXassigned exactly once inside the class'sstatic {}block. Init-once wiring, not mutable state. Six instances:createHeaders,createQueryParams,createRequest,createResponse,createRequestOptions,createRequestConditions.Object.freeze(this)once, at the end of every constructor. Freeze is shallow and never relied on tocascade — nested arrays and
Maps are frozen independently at build time (HTTP-5).newBuilder()deep-copies every collection, never aliases the source (HTTP-3). Value types with nobuilder (
Status,Protocol,MediaType,ETag,HttpRange) use static factories instead, per HTTP-3'sown carve-out.
requireField()single-sources HTTP-4's field-named errors so the`${name} is required`messagecannot drift between models.
DomainModelError; no barethrow new Error. Eachsets
this.name = new.target.name; wrap-and-rethrow always passes{cause}.