web-serial-rxjs API Documentation
    Preparing search index...

    Interface SerialSession

    Public API for interacting with the Web Serial API through a minimal, session-oriented surface.

    This interface is the swappable public contract for dependency injection and test fakes. Prefer typing application code against SerialSession and calling createSerialSession only at composition boundaries. Any object that structurally matches this shape (including a hand-written fake) is assignable to SerialSession; a separate SerialSessionLike-style alias is intentionally not exported.

    The session is intentionally slim so that apps (Angular, Vue, React, etc.) can drive their UI from state$ (canonical lifecycle state) + errors$ (error event channel) + receive$ + terminalText$ + lines$ and never have to rebuild BehaviorSubjects, manage a read loop, or serialize writes themselves.

    All imperative Web Serial work (open / read loop / write / close) is encapsulated by the implementation. Only Observables are exposed.

    import {
    createSerialSession,
    isWebSerialSupported,
    SerialSessionStatus,
    } from '@gurezo/web-serial-rxjs';

    if (!isWebSerialSupported()) {
    // fallback UI
    }

    const session = createSerialSession({ baudRate: 115200 });

    session.state$.subscribe((state) => {
    switch (state.status) {
    case SerialSessionStatus.Connected:
    console.log('connected:', state.portInfo);
    break;
    case SerialSessionStatus.Error:
    console.error('error:', state.error);
    break;
    }
    });
    session.receive$.subscribe((chunk) => console.log('rx:', chunk));
    session.errors$.subscribe((error) => console.error('err:', error));

    session.connect$().subscribe();
    session.send$('hello\r\n').subscribe();
    interface SerialSession {
        errors$: Observable<SerialError<SerialErrorCode>>;
        lines$: Observable<string>;
        receive$: Observable<string>;
        state$: Observable<SerialSessionState>;
        terminalText$: Observable<string>;
        connect$(): Observable<void>;
        disconnect$(): Observable<void>;
        dispose$(): Observable<void>;
        send$(data: SerialPayload): Observable<void>;
    }
    Index
    errors$: Observable<SerialError<SerialErrorCode>>

    Canonical fatal / non-fatal error channel.

    All SerialError instances produced by the session (connect / read / write / close) are multiplexed here. This is the main channel, not a supplementary one.

    Every emission is the exact same instance that is also surfaced to the relevant call-site subscriber (for example connect$().subscribe receives the same SerialError that errors$ emits for that failure), so a single subscription is enough to observe the full error history without double-normalisation.

    Fatal failures (connect / read / close) additionally drive state$ to 'error' and tear down the live pump + port; non-fatal failures (currently only send$ write failures) are multiplexed here without mutating state$, on the assumption that a real connection loss is detected by the read pump on the next tick.

    lines$: Observable<string>

    Decoded text split into complete lines using \n, \r\n, and lone interior \r (see implementation). Intended for logs, newline-framed command responses, and parsers—not for mirroring raw terminal output where \r must be preserved for progress/redraw. For rendering terminal text, prefer terminalText$.

    A trailing fragment without a line terminator is buffered until a later chunk completes a line, or discarded on disconnect. The incomplete tail is bounded by SerialSessionOptions.lineBuffer maxChars (default 1,048,576); when exceeded, leading characters are discarded and a non-fatal SerialErrorCode.LINE_BUFFER_OVERFLOW is emitted on errors$. Pass { maxChars: 0 } for unlimited growth. It is not subscription-lazy: the same framing runs whenever the read pump is active, independent of subscribers.

    receive$: Observable<string>

    Incoming data from the serial port as UTF-8 decoded text.

    The stream is driven by the read pump started by connect$ and is decoded internally with a streaming TextDecoder, so multi-byte characters split across chunks are joined correctly. It is not subscription-lazy: emissions happen regardless of whether a consumer is currently subscribed, so late subscribers see only new data.

    Emits raw decoder chunks (not line-aligned): carriage returns and other control characters from the peer are preserved. Use this for terminal-like mirrors, progress output that relies on \r, or raw inspection. Do not drive those UIs from lines$, which may split on interior \r and break redraw semantics.

    For newline-framed protocols, logs, or line-by-line parsing, prefer lines$ or derive custom framing from receive$.

    state$: Observable<SerialSessionState>

    Canonical lifecycle source for the session.

    Reactive session lifecycle state as a discriminated union. Replays the current state on subscribe. Switch on state.status and use the per-variant fields (portInfo when SerialSessionStatus.Connected, error when SerialSessionStatus.Error) instead of correlating separate streams.

    terminalText$: Observable<string>

    Terminal-display oriented cumulative text derived from receive$.

    This stream collapses carriage-return redraws (\r) and keeps normal newline behavior (\n, \r\n) so apps can bind terminal-like output directly without wrapping createTerminalBuffer in every consumer. By default, retains at most 10,000 completed lines and 1,048,576 characters; configure via SerialSessionOptions.terminalBuffer.

    Equivalent behavior:

    createTerminalBuffer(receive$).text$
    
    • Open a serial port and start the internal read pump.

      Returns an Observable that completes when the port is fully opened and the read pump is running. Subscribing to receive$ before calling connect$ is safe: emissions simply start after the pump is active. Unsubscribing during 'connecting' abandons that attempt. A port the abandoned attempt already opened is closed, and a later connect$() does not share its requestPort or open.

      Returns Observable<void>

      An Observable that completes on successful connection.

    • Close the active serial port and stop the internal read pump.

      Safe to call when already disconnected or while a disconnect is already in progress. When called during 'connecting', cancels the in-flight connect$() and returns the session to 'idle' without reaching 'connected'. If open() has already started, this Observable completes only after that attempt releases the port. state$ may emit 'idle' before that completion. If open() has not started, including while requestPort() is still pending, it completes without waiting. requestPort() cannot be aborted, so an open port picker stays up and a port chosen afterward is not opened.

      Returns Observable<void>

      An Observable that completes when an opened port is fully closed. During 'connecting', before open() starts, it completes once the attempt is cancelled.

    • Permanently tear down the session and complete all observables.

      dispose$ is cold. Calling it only returns an Observable. The port is not released, and state$ does not move to 'disposed', until that Observable is subscribed. Subscribe with subscribe(), or wait with an API that subscribes, such as firstValueFrom.

      Unlike disconnect$, which returns the session to 'idle' for reuse, a subscribed dispose$ closes any active connection, releases internal resources, emits 'disposed' on state$, and completes every session stream. After disposal, connect$ and send$ fail with SerialErrorCode.SESSION_DISPOSED; create a new session instead of reusing this instance.

      Safe to subscribe more than once; later subscriptions complete immediately.

      Returns Observable<void>

      An Observable that completes when disposal has finished. Nothing runs until it is subscribed.

      // Does nothing: no port release and no 'disposed' transition.
      session.dispose$();

      // Tears the session down.
      session.dispose$().subscribe({
      error: (error) => console.error(error),
      });
    • Enqueue data for ordered transmission.

      Writes are serialized internally through a FIFO send queue so that concurrent send$ calls are delivered to the port in call order, regardless of how quickly each subscriber runs. The bytes are fixed when the Observable is subscribed, not when send$() is called. String payloads are UTF-8 encoded via a shared TextEncoder at that moment. Uint8Array payloads are copied then and those bytes are written unchanged. Mutating the caller's buffer after subscribe does not change a queued write. Each subscription snapshots independently. Write failures are normalized into SerialError with SerialErrorCode.WRITE_FAILED and multiplexed on SerialSession.errors$ in addition to being surfaced to the subscriber, so a single subscription is enough to observe every I/O error. Calling send$ while the session is not in 'connected' state fails fast with SerialErrorCode.PORT_NOT_OPEN.

      The returned Observable completes once the enqueued payload has been flushed to the underlying writer.

      Unsubscribing before that write starts skips the payload. Unsubscribing after writer.write() has started stops next / error / complete for that subscriber only. The in-flight write is still awaited and is not aborted. takeUntil and timeout follow the same rule because they unsubscribe.

      disconnect$, dispose$, and a connect$ that is about to become 'connected' drop sends that have not started. Those subscribers error with SerialErrorCode.PORT_NOT_OPEN, and that failure is not emitted on SerialSession.errors$. The in-flight write is not aborted. The next send$ waits until it settles, so the writes do not overlap.

      Parameters

      • data: SerialPayload

        Text (UTF-8 encoded at subscribe) or raw bytes copied at subscribe.

      Returns Observable<void>

      An Observable that completes when the payload is written.