Repository navigation
relay: TCP listener, framing encoders and a stdlib Python client for the sidecar stream - #12
Merged
Merged
Conversation
…rts each close once
…against the real writers
…aits on a selector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
relay: TCP listener, framing encoders and a stdlib Python client for the sidecar stream
Summary
Adds the transport pieces an app needs to expose its backend stream over TCP, with no
policy of their own:
de_shell/js/main/framing.ts—encodeMessage/encodeBinary, the inverse ofstdoutDemux.ts. Byte-identical framing toipc.emitand anyplotlib'sencode_frame.de_shell/js/main/relay.ts—createRelay({host, port, onConnection, onLine, onClose, …}).Binds exactly the given address (an empty host is refused;
0.0.0.0only when asked),hands each client line to the app as a string (the first one included, so the app owns
admission), guards every connection until the app calls
admit()(hello timeout, asmall pre-admission line cap, a larger one after), writes framed units per connection
with the payload as its own chunk, exposes
writableLengthfor the app's backpressuredecisions, closes gracefully so a written refusal reaches the client, and sets keepalive
and no-delay on accepted sockets. Imports only
node:net, so it runs undernode --test.A
createServerfactory option leaves room for TLS without an API change.de_shell/remote_client.py— a synchronous, stdlib-only client:connect,send,recv(("message", dict)/("binary", header, bytes)/("stream", str)),close.recvwaits on a selector so asendon another thread is never cut short. Malformedunits raise
ValueErrorwith the offending bytes on.line, after being consumed.tests/test_boundary.pygains the new module; one towncrier fragment.Nothing in
backendProcess.tsorstdoutDemux.tschanges. Existing apps are unaffecteduntil they call the new exports.
Consumers
tokens, admission, the action filter and snapshots all stay in GC; the endpoint is a thin
use of
createRelay, and GC's laptop-side connector usesremote_client.the codec for its
serveprocess and connect mode; both stay yours.Deliberately not in this PR
Connect mode for Electron, a Python
serverelay, a subscriber API onbackendProcess(Ground Crew tees in its own handlers), any auth or policy, TLS itself (only the factory
hook), an
onServerErrorhook for post-listen server errors (currently swallowed soEMFILE cannot crash main — worth adding when an app wants the log line).
Two calls left to you: whether
hostshould additionally require an IP literal (ahostname is DNS-resolved to whichever address comes first); and, with TLS,
relay.close()would wait for sockets still in handshake, which the relay never sees — a doc note or a
close timeout when the hook is first used.
Tests
framing.test.ts(8): encoders decoded through the production demuxer, at several chunksizes, with marker look-alikes and
\ninside payloads, non-ASCII headers, non-finitenumbers refused.
relay.test.ts(32): real loopback sockets on port 0 — exact bind and OS-error rejection,empty host refused, lines across chunk splits and
\r\n, hello timeout and both caps onan injected clock, two clients delivered (no count of its own),
writableLengthunder astalled reader, graceful close delivering a queued refusal before EOF, destroy after the
grace,
relay.close()races, keepalive / no-delay /createServerinjection observedthrough a spy factory.
tests/test_remote_client.py(20): the decoder fed realipc.emit/encode_framebytesover a socketpair, whole and at 1- and 7-byte steps;
recvtimeout keeps partial bytes;EOF →
ConnectionError; malformed prefix/header/JSON →ValueErrorwith.line; a pollon one thread beside a blocking send on another; and a cross-runtime round trip against a
real
createRelayunder Node (skips by name whennodeis absent or cannot strip types).Evidence
npm run typecheckclean ·npm run test:unit133/133 ·
pytest117 passed, 0 skipped · wheel carries the three new source files.npm run typecheckclean ·npm run test:unit133/133 ·
pytest117 passed, 0 skipped. (npm 11 withheld electron's postinstall underallow-scripts; neither suite needs the binary.)
Release
New public API on both halves; a 0.4.0 release would let apps pin it
(
de-shell>=0.4,<0.5). The fragment isupcoming_changes/+relay.new_feature.rst; it willbe renamed to
<this PR's number>.new_feature.rstonce the number exists.