Issie logo Issie

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

Type/Module Description

Constants

SidecarFailure

SidecarFailureKind

Why a sidecar command did not answer with what was asked for. **Two kinds, because only two things can be done about one.** A `Fault` is the sidecar breaking - an exception escaped a handler, so an invariant the simulator maintains no longer holds. That is a bug in Issie: it is logged as an error, which in a debug build puts it and its stack on the screen. A `Refusal` is the sidecar declining something it anticipated and wrote a message for - a stale epoch, a cycle it has not run to, a sheet that did not decode - which the caller is expected to handle, and which is a warning unless the caller cannot. **Anything unclassified is a Fault.** A failure nobody has said is expected is a bug until somebody says otherwise; the alternative default quietly downgrades whatever is forgotten. The strings are Protocol.FaultKind and Protocol.RefusalKind, mirrored here because the renderer cannot see that file - the sidecar references the renderer, not the other way round.

Functions and values

Function or value Description

byteLength bytes

Full Usage: byteLength bytes

Parameters:
    bytes : obj

Returns: float
bytes : obj
Returns: float

combineFailures failures

Full Usage: combineFailures failures

Parameters:
Returns: SidecarFailure

Several failures as one, for an operation that made several requests and lost more than one of them. **A fault among them makes the whole a fault**: the worst news decides what anyone has to do about it, and a real bug must not be hidden by the refusals it arrived with.

failures : SidecarFailure list
Returns: SidecarFailure

connect ()

Full Usage: connect ()

Parameters:
    () : unit

Returns: Promise<unit>

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.

() : unit
Returns: Promise<unit>

connectionState ()

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.

() : unit
Returns: bool * int

epochOf buildReply

Full Usage: epochOf buildReply

Parameters:
    buildReply : string

Returns: int

The session epoch a build reply issued, or 0 if it issued none. Zero for an error reply and for anything unparseable, which is the safe direction: a command naming epoch 0 is refused by the sidecar unless there genuinely is no session, so a caller that failed to build cannot go on to name one.

buildReply : string
Returns: int

errorOfFrame frame

Full Usage: errorOfFrame frame

Parameters:
    frame : obj

Returns: SidecarFailure option
frame : obj
Returns: SidecarFailure option

failureOfPayload payload

Full Usage: failureOfPayload payload

Parameters:
    payload : string

Returns: SidecarFailure

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.

payload : string
Returns: SidecarFailure

fault message

Full Usage: fault message

Parameters:
    message : string

Returns: SidecarFailure

A failure nothing declined: the socket went, a request was never answered, or a reply broke the contract it was supposed to keep. Not something any handler foresaw, so it is a fault - which is also the default for anything unclassified, for the same reason.

message : string
Returns: SidecarFailure

logFailure context failure

Full Usage: logFailure context failure

Parameters:

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.

context : string
failure : SidecarFailure

makeBytes size

Full Usage: makeBytes size

Parameters:
    size : int

Returns: obj
size : int
Returns: obj

prefixFailure context failure

Full Usage: prefixFailure context failure

Parameters:
Returns: SidecarFailure

The same failure, said with the context the caller can add. **Keeps the kind**: putting "reading the step panel: " in front of a fault does not make it a refusal, and the commonest way to lose a classification is to rebuild the value as a string on the way past.

context : string
failure : SidecarFailure
Returns: SidecarFailure

readUint32At bytes offset

Full Usage: readUint32At bytes offset

Parameters:
    bytes : obj
    offset : int

Returns: float

Read a uint32 little-endian at a byte offset of a frame.

bytes : obj
offset : int
Returns: float

request cmd payload

Full Usage: request cmd payload

Parameters:
    cmd : int
    payload : obj

Returns: Promise<obj>

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.

cmd : int
payload : obj
Returns: Promise<obj>

sendDesign topSheet sheetJsons

Full Usage: sendDesign topSheet sheetJsons

Parameters:
    topSheet : 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.

topSheet : string
sheetJsons : string list
Returns: Promise<string>

simBuild maxArraySize

Full Usage: simBuild maxArraySize

Parameters:
    maxArraySize : int

Returns: Promise<string>

Build a simulation of the last-sent design's top sheet on the sidecar.

maxArraySize : int
Returns: Promise<string>

simDigest ticks

Full Usage: simDigest ticks

Parameters:
    ticks : int

Returns: Promise<string>

The sidecar's deterministic-stimulus digest text for the last-sent design (an error reply starts with '{').

ticks : int
Returns: Promise<string>

simEnd epoch

Full Usage: simEnd epoch

Parameters:
    epoch : int

Returns: Promise<string>
epoch : int
Returns: Promise<string>

simLog ()

Full Usage: simLog ()

Parameters:
    () : unit

Returns: Promise<string>

The sidecar's SimLog ring as JSON - the .NET half of a cross-runtime cost comparison.

() : unit
Returns: Promise<string>

simPorts epoch path

Full Usage: simPorts epoch path

Parameters:
    epoch : 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.

epoch : int
path : int list
Returns: Promise<Result<ComponentSlots list, SidecarFailure>>

simRead epoch startCycle rep samples signals

Full Usage: simRead epoch startCycle rep samples signals

Parameters:
    epoch : 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.

epoch : int
startCycle : int
rep : int
samples : int
signals : (int * int * int list) list
Returns: Promise<obj>

simReadDrivers epoch startCycle rep samples drivers

Full Usage: simReadDrivers epoch startCycle rep samples drivers

Parameters:
    epoch : int
    startCycle : int
    rep : int
    samples : int
    drivers : DriverIndex list

Returns: Promise<obj>

SimRead by driver handle: the indices the port slice handed out, valid for this build. Resolves with the raw response frame, exactly as `simRead` does - same reply layout, same zero-copy view - so the two are interchangeable to everything downstream.

epoch : int
startCycle : int
rep : int
samples : int
drivers : DriverIndex list
Returns: Promise<obj>

simReadRam epoch cycle compId path sparseUpTo start rows

Full Usage: simReadRam epoch cycle compId path sparseUpTo start rows

Parameters:
    epoch : 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.

epoch : int
cycle : int
compId : int
path : int list
sparseUpTo : int
start : bigint
rows : int
Returns: Promise<Result<RamView, SidecarFailure>>

simReadWordsPerSample frame

Full Usage: simReadWordsPerSample frame

Parameters:
    frame : obj

Returns: int

How many uint32 words each sample of a SimRead reply occupies - ceil(widest signal / 32), so one for a reply of ordinary buses. At byte 16 of the frame: 8 of frame header, then the signal and sample counts.

frame : obj
Returns: int

simRun epoch targetCycle timeoutMs

Full Usage: simRun epoch targetCycle timeoutMs

Parameters:
    epoch : 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.

epoch : int
targetCycle : int
timeoutMs : int
Returns: Promise<string>

simSetInputs epoch cycle values

Full Usage: simSetInputs epoch cycle values

Parameters:
    epoch : int
    cycle : int
    values : (int * float) list

Returns: Promise<string>

Set top-level input values at a cycle: (component id, value) pairs, values up to 2^53 (split into low and high words on the wire). Reply is JSON.

epoch : int
cycle : int
values : (int * float) list
Returns: Promise<string>

transportFailure message

Full Usage: transportFailure message

Parameters:
    message : string

Returns: SidecarFailure

A transport failure: the socket went, or the request was never answered.

message : string
Returns: SidecarFailure

uint32At view index

Full Usage: uint32At view index

Parameters:
    view : obj
    index : int

Returns: float

Read an element of a Uint32Array view.

view : obj
index : int
Returns: float

unwrapError payload

Full Usage: unwrapError payload

Parameters:
    payload : string

Returns: string

Just the message, for a caller that has already decided what the failure means.

payload : string
Returns: string

viewSimReadData frame count

Full Usage: viewSimReadData frame count

Parameters:
    frame : obj
    count : int

Returns: obj

A zero-copy Uint32Array view over `count` words starting at byte 24 of a SimRead response frame - 8 of frame header, three uint32 counts and four bytes of padding, which is what keeps the values 8-aligned.

frame : obj
count : int
Returns: obj

writeUint32At bytes offset value

Full Usage: writeUint32At bytes offset value

Parameters:
    bytes : obj
    offset : int
    value : float

Write a uint32 little-endian, as the wire wants it, at a byte offset.

bytes : obj
offset : int
value : float

Type something to start searching.