feat(deploy): direct-HTTPS peer relay for streamed deploy_component#146
feat(deploy): direct-HTTPS peer relay for streamed deploy_component#146kriszyp wants to merge 10 commits into
Conversation
Replicating a `deploy_component` with a streamed payload no longer goes
through the WebSocket sendOperation path — that wraps the entire
operation (including the payload Buffer) in a single WS frame, which
can't carry payloads beyond Node's 2 GB cap.
When core's deployComponent has staged the payload to a temp file (see
the corresponding core change at feat/deploy-component-payload-staging),
replicateOperation now relays to each peer over direct HTTPS:
1. Mint a short-lived operation token by calling
`create_authentication_tokens` over the existing replication WS
connection — the peer signs a token tied to whatever user authed
that WS connection, so the auth model matches existing replication.
2. Open HTTPS to the peer's operations API (port 9925 by default,
overridable per node) and stream a multipart/form-data deploy
request with the staged payload as the file part. Peer processes
it as a normal local deploy with `replicated: false` so it doesn't
re-fan-out.
3. Per-peer success/failure is aggregated with Promise.allSettled —
one peer failing doesn't abort the others, matching the per-peer-
status semantics agreed for HarperFast/harper#524.
Out of scope for this slice:
- Transient-error retries per peer (basic fail-fast; can refine after
we see real-world failure modes)
- SSE re-emission of per-peer events on the origin's response stream
(the SSE channel is in core via #531; the relay can plug into it as
a follow-up)
- restart_service relay (small payload, WS path is fine)
Bumps the core submodule pointer to pick up the payload staging
required for this to work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| hostname: target.hostname, | ||
| port: target.port || (target.protocol === 'https:' ? 443 : 80), | ||
| method: 'POST', | ||
| path: '/', |
There was a problem hiding this comment.
The path is hardcoded to '/' instead of using target.pathname. When node.operationsApiUrl is set to a URL with a non-root path (e.g. http://proxy:9925/ops/), the request silently goes to / instead — the intended path prefix is dropped entirely.
The comment on resolveOperationsApiUrl explicitly says this field is "used by tests and by deployments that put the ops API behind a proxy", so path-based proxies are a supported scenario.
| path: '/', | |
| path: target.pathname, |
|
Reviewed; no blockers found. |
…-peer-relay # Conflicts: # core
Cascades the fix from #531 (a320af514): drain the IncomingMessage when useSse=true but server returns non-SSE content (e.g. 401 auth failure). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…error stringify fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pausing this PR — converting to draft. This work is being rebuilt on top of the design in #641: a replicated In the new design:
This work resumes as part of Slice B in #641 once Slice A (table + blob-backed multipart receive) lands. — Claude |
|
Closing in favor of the deployment-tracking redesign tracked in HarperFast/harper#641, now in flight as Slices A (HarperFast/harper#655, merged), B1 (HarperFast/harper#657, merged), and B2 (HarperFast/harper#758). This PR's direct-HTTPS peer relay was solving the operations-API frame cap for The harper-pro-side simplification will land alongside HarperFast/harper#758 as a separate small PR in this repo. 🤖 Generated by Claude |
Summary
Final slice of HarperFast/harper#524. Replicating a streamed `deploy_component` no longer goes through the WebSocket `sendOperation` path — which wraps the whole operation (including the payload Buffer) into a single WS frame and so can't carry payloads beyond Node's 2 GB cap. Instead, the origin opens a direct HTTPS connection to each peer's operations API and streams the same `multipart/form-data` body the CLI used.
How it works
When core's `deployComponent` has staged the payload to a temp file (see HarperFast/harper#536), `replicateOperation` detects the streaming-deploy case and dispatches to `relayDeployToNode` instead of `sendOperationToNode`:
Where to look
`replication/deployRelay.ts` — the relay function. Notable design points:
`replication/replicator.ts` — small branch in `replicateOperation`. The new path is gated on `req.operation === 'deploy_component' && typeof req._stagedPayloadPath === 'string'` and the dynamic import of `./deployRelay.ts` keeps the relay code out of every callsite that doesn't need it.
`unitTests/replication/deployRelay.test.mjs` — `node --test` unit tests. Spins up a local HTTP server playing the role of a peer, stubs `mintToken`, and verifies wire format end-to-end. Covers: happy path (multipart streamed correctly, JSON response parsed, peer cannot re-replicate), 5xx response surfaced as `{ status: 'failed' }` with status code, token-mint failure (no HTTPS attempt made), missing staged payload.
What's deliberately deferred (called out as follow-ups in the commit message)
Test plan
Related
🤖 Generated with Claude Code