SidecarClient Module
The renderer's half of the sidecar transport: connect, then correlated request -> response.
Wire protocol, shared with src/Sidecar/Protocol.fs - the two files change together:
byte 0 command; a response carries the request's command with bit 7 set, and bit 6
as well when its payload is an error message rather than the answer
bytes 1..4 correlation id, uint32 little-endian, echoed back by the sidecar
bytes 5..7 padding, always zero: an 8-byte header means a binary response payload
starts 8-aligned, so Uint32Array/Float64Array views need no copy
bytes 8.. payload
The socket is the browser WebSocket global: the renderer is sandboxed and a browser API is
what it has - which is also why no npm package is involved. Where the socket points comes
from Bridge.sidecarEndpoint, polled at the moment of connecting.
Frames are Uint8Array over ArrayBuffer, reached only through the Emit helpers below: sizes
and correlation ids stay as plain floats (JS numbers), and nothing indexes a typed array
through an F# array type.
Types and nested modules
Functions and values
| Function or value | Description |
Full Usage:
byteLength bytes
Parameters:
obj
Returns: float
|
|
Full Usage:
combineFailures failures
Parameters:
SidecarFailure list
Returns: SidecarFailure
|
|
|
Resolves once the socket is open, WAITING for the sidecar to be listening if it is still starting. One socket per renderer: a second connect while one is open resolves immediately, and one made while another is still connecting waits on that one. Rejects only when the sidecar is genuinely not there - it never started, or it has died - which is a fault and is reported as one. That distinction is the point of the wait above: everything over this is written as though the sidecar is simply there, because by the time it is called it is.
|
Full Usage:
connectionState ()
Parameters:
unit
Returns: bool * int
|
Whether a socket is open, and how many requests are waiting on the sidecar. For the development harness, which is the only thing that asks: "is the sidecar there yet" is otherwise unanswerable from outside, and the honest answer is what tells a test that has just started the app to wait rather than to conclude the simulator is broken.
|
Full Usage:
epochOf buildReply
Parameters:
string
Returns: int
|
|
|
|
|
The message inside an error payload, which is a JSON object whose only key is "error". **Unwrapped here, so that no reader ever sees the envelope.** Readers used to take the payload as it arrived, so a sidecar failure was logged as the literal `{"error":"..."}`. That was merely untidy while the payload was one line; now that a stack trace travels in it, the difference is between a readable trace and a run of escaped `\n`s. A payload that is not the expected shape is returned as it came: this is the error path, and something unreadable is better than nothing.
|
|
|
|
Report a failure at the severity its kind calls for, which is the whole reason the kind exists. A fault is Issie broken and is an error - so in a debug build it is on the screen with its stack a moment later. A refusal is the sidecar declining something a caller was supposed to be ready for, so it is a warning: it belongs in the bug report and interrupts nobody. The one call every reader of a sidecar failure should make, rather than choosing `Log.error` for itself: choosing by hand is how the two came to mean the same thing.
|
Full Usage:
makeBytes size
Parameters:
int
Returns: obj
|
|
Full Usage:
prefixFailure context failure
Parameters:
string
failure : SidecarFailure
Returns: SidecarFailure
|
|
Full Usage:
readUint32At bytes offset
Parameters:
obj
offset : int
Returns: float
|
|
|
One request, resolved with the whole response frame - header included, so a caller can size what came back. The payload is a Uint8Array from makeBytes. **Connects if there is no socket**, rather than refusing. A closed connection is a normal event - the sidecar drops one when a handler faults, and the process behind it goes on holding the session - so "connect first" as a permanent answer was wrong in the way that matters: only a BUILD ever called `connect`, so after a drop every run and every read failed, the viewer retried on its backoff for ever, and the simulation stayed dead until the user happened to press Refresh. Reconnecting here costs nothing when a socket is open (one match) and is the whole of the recovery when there is not. It cannot storm: `connect` is single-flight, so concurrent callers join one attempt, and it rejects rather than looping when the sidecar is genuinely gone.
|
Full Usage:
sendDesign topSheet sheetJsons
Parameters:
string
sheetJsons : string list
Returns: Promise<string>
|
Send a design as ONE MESSAGE PER SHEET: which sheet of how many, then the top sheet's name and that sheet's JSON. Per sheet because decoding is the cost and it happens on the sidecar's serve loop, which serves one message at a time - so a whole design in one message is one handler holding that loop for ~300ms on 3cpu, against ~25ms for its largest single sheet. Per-sheet framing also lets the sidecar reuse sheets it has already decoded, since an unchanged sheet serialises to the identical string. Sent in order, awaited one at a time. The sheets are a design only once the last has landed, which is what the reply's `complete` says; sending them at once would arrive in any order and give the sidecar no way to know when it had them all. A design is only ever sent with every simulation closed - Start and Refresh both do it on a closed one - so an upload never races a session. The sidecar drops whatever session it holds when the first sheet arrives, so a command left over from before the design changed names an epoch that no longer exists. Resolves with the last reply, or stops at the first error and resolves with that.
|
|
|
|
|
|
|
Full Usage:
simPorts epoch path
Parameters:
int
path : int list
Returns: Promise<Result<ComponentSlots list, SidecarFailure>>
|
Width and driver index of every port of every component on one instance's sheet - the wave selector's read, made when its combo boxes pick an instance. Decoded into the SAME type the renderer's own simulator answers with (PortView.sheetSliceOf), so everything downstream is one code path and only the source of the bytes differs.
|
Full Usage:
simRead epoch startCycle rep samples signals
Parameters:
int
startCycle : int
rep : int
samples : int
signals : (int * int * int list) list
Returns: Promise<obj>
|
THE waveform-data interface: for each signal - (component id, output port number, access path root-first) - read `samples` values taken every `rep` cycles from `startCycle`. These are the same (StartCycle, SamplingZoom, ShownCycles) parameters the waveform viewer's own generation runs on, so a view at any zoom is one request. Resolves with the raw response frame: on success the values are `viewSimReadData frame (signals * samples * simReadWordsPerSample frame)`, signal-major and zero-copy; an error response is one `errorOfFrame` answers for. The word count is part of it and not a detail. A sample is as many uint32s as the widest signal asked for needs, so a request carrying one wide bus widens every sample in that reply - and a caller sizing the view as `signals * samples` reads a fraction of what was sent, silently.
|
Full Usage:
simReadDrivers epoch startCycle rep samples drivers
Parameters:
int
startCycle : int
rep : int
samples : int
drivers : DriverIndex list
Returns: Promise<obj>
|
|
Full Usage:
simReadRam epoch cycle compId path sparseUpTo start rows
Parameters:
int
cycle : int
compId : int
path : int list
sparseUpTo : int
start : bigint
rows : int
Returns: Promise<Result<RamView, SidecarFailure>>
|
One memory's contents at one clock, as a RAM table shows them. `sparseUpTo` is the most non-zero locations worth listing; past that a window of `rows` from `start` comes back instead, and zero asks for a window whatever the memory holds. Which of the two arrives is the sidecar's decision - only it knows how much the memory holds - so the reply says which it is and the caller draws accordingly.
|
Full Usage:
simReadWordsPerSample frame
Parameters:
obj
Returns: int
|
|
Full Usage:
simRun epoch targetCycle timeoutMs
Parameters:
int
targetCycle : int
timeoutMs : int
Returns: Promise<string>
|
Advance the sidecar's simulation towards a cycle within a millisecond budget (0 = none); the reply says where the clock got to. Chunk by repeating; cancel by stopping. Every command that depends on a session names the session it means, and the sidecar refuses one that names any other - see SimSession.checkEpoch. That is what stops a reply from a superseded simulation being taken for a reply from this one.
|
Full Usage:
simSetInputs epoch cycle values
Parameters:
int
cycle : int
values : (int * float) list
Returns: Promise<string>
|
|
|
|
Full Usage:
uint32At view index
Parameters:
obj
index : int
Returns: float
|
|
Full Usage:
unwrapError payload
Parameters:
string
Returns: string
|
|
Full Usage:
viewSimReadData frame count
Parameters:
obj
count : int
Returns: obj
|
|
Full Usage:
writeUint32At bytes offset value
Parameters:
obj
offset : int
value : float
|