feat(npm): streampub CAS sink + attachment gate (S11 Step 15, 1/3)

📦 What

S11 Step 15 (publish handler - stream + coherence + CAS) ships as a three-MR stack, in dependency order:

  • !695 (merged) - streampub CAS sink + attachment gate (library seam + unit tests). <- this MR
  • !696 (merged) - publish handler streaming/CAS staging (Spec Step 3): gate-driven pre-check, lazy session, commit.
  • !697 (merged) - publish Step 4 coherence check (shasum / integrity / manifest).

This MR is the bottom of the stack and targets main. Slices 2/3 and 3/3 build on it.

Slice 1 - the streampub seam the publish handler stages tarballs through. Library-only: no handler wiring yet, so Step 14's existing Process call is unchanged (a nil gate keeps the two-arm fan-out).

  • WithCASSink(io.Writer) threads the decoded tarball to a third io.MultiWriter arm; the Result digests are byte-for-byte the digests of what the sink received (so the server-computed SHA-256 is the commit authority).
  • WithAttachmentGate(GateFunc) fires at the attachment hand-off - once the envelope identity is parsed and before any tarball bytes stream - and returns the sink or an error that aborts the pass. This is what lets slice 2/3 run the version-exists / quota pre-check before opening a session (spec Step 2). WithCASSink is the unconditional form of the gate.
  • casSinkWriter captures the sink's first write error verbatim (errors.Is fidelity, precedence over the cascaded inspector error); a short write is treated as a storage error.

Spec coverage

Behaviour Test
CAS sink receives the decoded tarball byte-for-byte; digests match those bytes TestProcess_WithCASSink_ThreadsDecodedTarball
Nil sink preserves the two-arm behavior (Step 14 Process unchanged) TestProcess_NilCASSink_PreservesTwoArmBehavior
CAS sink is transparent to digests / captured package.json TestProcess_CASSink_Transparent
CAS-write error surfaces from Process via errors.Is TestProcess_CASSink_WriteError_Surfaces
CAS-write error wins precedence over a coherence/size error TestProcess_CASSink_WriteError_WinsPrecedence
Gate receives the parsed identity; the returned sink streams the tarball TestProcess_AttachmentGate_ReceivesIdentity
Gate error aborts the pass before any bytes stream TestProcess_AttachmentGate_AbortsBeforeStreaming
Gate receives a clone of dist-tags; a gate mutation can't corrupt the Result TestProcess_AttachmentGate_ClonesDistTags

BenchmarkProcess_WithCASSink / _NoSink record the per-publish allocation baseline.

🧪 Testing

  • go test ./internal/format/npm/... - pass
  • golangci-lint run ./internal/format/npm/... - 0 issues
  • go vet -tags integration ./internal/format/npm/... - clean
  • Unit-level only (no DB); the gate/sink are exercised end-to-end against the real storage Session in !696 (merged) / !697 (merged).

Related to #122 (closed)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading