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

    Migrating to v3

    v3 introduces two TypeScript-facing breaking changes:

    1. SerialErrorCodeenum → const object + union type (runtime values unchanged).
    2. state$ payload — flat string → discriminated union with per-status detail.

    This guide covers both. Runtime string values for error codes are unchanged (SerialErrorCode.READ_FAILED is still 'READ_FAILED').

    import {
    SerialError,
    SerialErrorCode,
    SerialSessionStatus,
    type SerialSessionState,
    } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state: SerialSessionState) => {
    switch (state.status) {
    case SerialSessionStatus.Connected:
    console.log(state.portInfo);
    break;
    case SerialSessionStatus.Error:
    console.error(state.error);
    break;
    }
    });

    session.errors$.subscribe((error) => {
    if (error.is(SerialErrorCode.READ_FAILED)) {
    console.error(error.context.cause);
    }
    });

    Phase 1 of #472 removed duplicate / escape-hatch APIs so state$ and dispose$() are the only public sources for lifecycle and teardown. See #478 for the documentation pass.

    Removed API Replacement
    destroy$() dispose$()
    isConnected$ state$ with state.status (or derive a boolean from state$)
    portInfo$ state.portInfo when state.status === SerialSessionStatus.Connected
    getPortInfo() state.portInfo when connected (same as above)
    getCurrentPort() No direct replacement. Use state.portInfo for identification; raw SerialPort is not exposed

    Details: §4, §5, §6, §7.


    v2 v3
    export enum SerialErrorCode { ... } export const SerialErrorCode = { ... } as const + export type SerialErrorCode
    TypeDoc: enums/SerialErrorCode.html TypeDoc: variables/SerialErrorCode.html
    • SerialErrorCode.BROWSER_NOT_SUPPORTED (and any other member)
    • error.code === SerialErrorCode.WRITE_FAILED
    • error.is(SerialErrorCode.LINE_BUFFER_OVERFLOW) with narrowed context
    • switch (error.code) { case SerialErrorCode.READ_FAILED: ... }
    • Type-only imports — continue using import type { SerialErrorCode } from '@gurezo/web-serial-rxjs'.
    • TypeDoc deep links — update bookmarks from enums/SerialErrorCode.html to variables/SerialErrorCode.html.
    • Tools parsing .d.ts — declaration shape changes from enum to const + type alias.

    v2 v3
    state$: Observable<'idle' | 'connected' | ...> state$: Observable<SerialSessionState> (discriminated union)
    SerialSessionState const (string literals) SerialSessionStatus const (string literals)
    Compare state === SerialSessionState.Connected Compare state.status === SerialSessionStatus.Connected
    Correlate state$ + portInfo$ / errors$ manually connected carries portInfo; error carries SerialError
    import { SerialSessionState } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state) => {
    if (state === SerialSessionState.Connected) {
    session.getPortInfo(); // separate call
    }
    });
    import { SerialSessionStatus } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state) => {
    switch (state.status) {
    case SerialSessionStatus.Connected:
    console.log(state.portInfo);
    break;
    case SerialSessionStatus.Error:
    console.error(state.error);
    break;
    }
    });
    export const SerialSessionStatus = {
    Idle: 'idle',
    Connecting: 'connecting',
    Connected: 'connected',
    Disconnecting: 'disconnecting',
    Unsupported: 'unsupported',
    Error: 'error',
    Disposed: 'disposed',
    } as const;

    export type SerialSessionState =
    | { readonly status: typeof SerialSessionStatus.Idle }
    | { readonly status: typeof SerialSessionStatus.Connecting }
    | { readonly status: typeof SerialSessionStatus.Connected; readonly portInfo: SerialPortInfo }
    | { readonly status: typeof SerialSessionStatus.Disconnecting }
    | { readonly status: typeof SerialSessionStatus.Unsupported }
    | { readonly status: typeof SerialSessionStatus.Error; readonly error: SerialError }
    | { readonly status: typeof SerialSessionStatus.Disposed };
    • [ ] Replace import { SerialSessionState } used as constants with SerialSessionStatus.
    • [ ] Replace state === SerialSessionState.X with state.status === SerialSessionStatus.X.
    • [ ] Replace switch (state) with switch (state.status) (or compare state.status in if).
    • [ ] Use state.portInfo when state.status === SerialSessionStatus.Connected (portInfo$ and getPortInfo() were removed — see §5).
    • [ ] Use state.error when state.status === 'error' (same instance as errors$ for fatal errors).
    • errors$ remains available as the independent error event channel.
    • Lifecycle convenience APIs (portInfo$, getPortInfo(), isConnected$, destroy$()) are removed — migrate with §4§6.

    v3.0.0 introduced typed SerialError.context. For cause-bearing error codes, context.cause is the canonical source for the underlying failure.

    SerialError.originalError and the legacy constructor third argument remain in v3.x for backward compatibility but are deprecated and scheduled for removal in the next major version.

    session.errors$.subscribe((error) => {
    if (error.code === SerialErrorCode.READ_FAILED) {
    console.error(error.originalError);
    }
    });
    session.errors$.subscribe((error) => {
    if (error.is(SerialErrorCode.READ_FAILED)) {
    // error.context.cause is unknown — non-Error throws are preserved
    console.error(error.context.cause);
    }
    });
    • [ ] Replace error.originalError with error.context.cause (narrow with error.is(code) first).
    • [ ] If you construct errors with new SerialError(code, message, cause), switch to new SerialError(code, message, undefined, { cause }).
    • [ ] Address TypeScript @deprecated warnings by migrating to the patterns above.
    • originalError remains available in v3.x.
    • When context.cause is an Error instance, originalError is kept in sync for legacy callers.
    • context.cause is typed as unknown because JavaScript allows throwing non-Error values.

    destroy$() was a legacy alias of dispose$(). Lifecycle terminology (dispose, disposed, SESSION_DISPOSED) already used dispose$ as the canonical API. Phase 1 (#473 / #479) removed destroy$() from the public API so session teardown has a single entry point.

    session.destroy$().subscribe({
    complete: () => console.log('session destroyed'),
    });
    session.dispose$().subscribe({
    complete: () => console.log('session disposed'),
    });
    • [ ] Replace session.destroy$() with session.dispose$().
    • [ ] Prefer dispose$ in new code and documentation.

    Keeping both names forced callers to choose between equivalent APIs and kept docs / tests dual. dispose$() is the only teardown method.


    v3.0.0 made state$ a discriminated union. When state.status is SerialSessionStatus.Connected, state.portInfo is the canonical source for the active port's SerialPort.getInfo() snapshot — TypeScript narrowing guarantees it is present.

    portInfo$ and getPortInfo() exposed SerialPortInfo | null, which did not encode the relationship between connection state and port information. Phase 1 removed both APIs (#473 / #479).

    session.portInfo$.subscribe((portInfo) => {
    if (portInfo) {
    console.log(portInfo);
    }
    });

    const snapshot = session.getPortInfo();
    import { SerialSessionStatus } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state) => {
    if (state.status === SerialSessionStatus.Connected) {
    console.log(state.portInfo);
    }
    });
    • [ ] Replace portInfo$ subscriptions with state$ and read state.portInfo when state.status === SerialSessionStatus.Connected.
    • [ ] Replace getPortInfo() with state$ narrowing and state.portInfo.
    • [ ] Prefer state.portInfo in new code and documentation.
    • errors$ is not a duplicate of lifecycle state — it remains the independent error event channel.

    v3.0.0 made state$ a discriminated union. When state.status is SerialSessionStatus.Connected, TypeScript narrowing gives type-safe access to state.portInfo and other state-specific fields.

    isConnected$ was an Observable<boolean> that only projected whether the session was connected, so it lost the type information carried by the discriminated union and could not distinguish idle / connecting / disconnecting / error / disposed. Phase 1 removed it (#473 / #479).

    session.isConnected$.subscribe((isConnected) => {
    if (isConnected) {
    // session state is not narrowed
    }
    });
    import { SerialSessionStatus } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state) => {
    if (state.status === SerialSessionStatus.Connected) {
    // state.portInfo and other connected fields are available
    }
    });
    import { distinctUntilChanged, map } from 'rxjs';
    import { SerialSessionStatus } from '@gurezo/web-serial-rxjs';

    const isConnected$ = session.state$.pipe(
    map((state) => state.status === SerialSessionStatus.Connected),
    distinctUntilChanged(),
    );

    The local isConnected$ above is application-derived from state$. It is not a SerialSession member.

    When you need portInfo or other connected-only fields inside a pipeline, use isConnectedSessionState with filter(). Inline filter((s) => s.status === SerialSessionStatus.Connected) does not narrow types in TypeScript.

    import { filter } from 'rxjs';
    import { isConnectedSessionState } from '@gurezo/web-serial-rxjs';

    session.state$
    .pipe(filter(isConnectedSessionState))
    .subscribe((state) => {
    console.log(state.portInfo);
    });
    import { computed } from '@angular/core';
    import { toSignal } from '@angular/core/rxjs-interop';
    import { SerialSessionStatus } from '@gurezo/web-serial-rxjs';

    const sessionState = toSignal(session.state$);

    const isConnected = computed(
    () => sessionState().status === SerialSessionStatus.Connected,
    );
    • [ ] Replace isConnected$ subscriptions with state$ and narrow on state.status === SerialSessionStatus.Connected.
    • [ ] When you only need a boolean for UI, derive it from state$ with map or computed.
    • [ ] Prefer state$ narrowing in new code and documentation.

    SerialSession.getCurrentPort() was a raw SerialPort escape hatch. Calling port.close() or writable.getWriter() on the returned port could conflict with the session lifecycle and break internal runtime invariants. There is no direct replacement — the library does not expose the managed SerialPort so callers cannot bypass session I/O.

    A usage audit (#437) found no production callers in this repository. Device identification is covered by state.portInfo, so getCurrentPort() has been removed from the public API (#448). Phase 1 parent issue #472 / child issue #474 also track this removal as a completion criterion.

    Area Finding
    Library production code No getCurrentPort() callers
    Example apps Test mocks only
    Device identification alternative state.portInfo after state$ narrowing (canonical)
    Signals (DTR/RTS, etc.) No replacement API yet (future feature addition)
    const port = session.getCurrentPort();
    if (port) {
    console.log(port.getInfo());
    }
    import { SerialSessionStatus } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state) => {
    if (state.status === SerialSessionStatus.Connected) {
    console.log(state.portInfo);
    }
    });

    Operations such as getSignals() / setSignals() that previously required a raw port have no SerialSession replacement yet. If you need them, open a separate issue to propose first-class APIs.

    • [ ] Remove all getCurrentPort() calls.
    • [ ] Use state$ narrowed on SerialSessionStatus.Connected and read state.portInfo for device identification.
    • [ ] If you depend on signals or other native operations, request a dedicated API via an issue.

    Some members of the public SerialErrorCode contract were not emitted by the v3.x runtime. To prevent unreachable error-handling branches, all 19 codes were audited (#438) and the results are recorded here and in the API Reference.

    Category Count Description
    Implemented 15 Emitted at runtime in v3.x / v4 (or thrown at factory time)
    Reserved 2 Present in the public API but not emitted; scheduled for removal in the next major version
    Removed in v4 Phase 2 2 Receive-replay codes deleted with receiveReplay$ / receiveReplay
    Code Reason Alternative
    PORT_NOT_AVAILABLE Current implementation uses only navigator.serial.requestPort; no getPorts API path exists Use PORT_OPEN_FAILED or OPERATION_CANCELLED for port acquisition failures
    OPERATION_TIMEOUT No timeout / prompt detection / transaction API yet None (revisit when a future API is added)

    v3.x adds @deprecated annotations only; runtime values and exports are unchanged. Removal is deferred to the next major version.

    Code Emit location fatal / non-fatal context Tests
    BROWSER_NOT_SUPPORTED connect$ (no navigator.serial) non-fatal undefined integration
    PORT_OPEN_FAILED connect$ (port.open() reject) fatal { cause } integration
    PORT_ALREADY_OPEN connect$ (not in 'idle' / 'error') non-fatal undefined integration
    PORT_NOT_OPEN send$ / disconnect$ (invalid state) non-fatal undefined integration
    READ_FAILED read pump error fatal { cause } integration
    WRITE_FAILED send$ write failure non-fatal { cause } integration
    CONNECTION_LOST port.close() failure / stream drop fatal { cause } integration
    INVALID_FILTER_OPTIONS createSerialSession factory throw ValidationErrorContext unit + integration
    OPERATION_CANCELLED requestPort dialog cancelled fatal { cause } integration
    LINE_BUFFER_OVERFLOW lines$ tail overflow non-fatal { maxChars } integration
    INVALID_TERMINAL_BUFFER_OPTIONS factory throw ValidationErrorContext unit
    INVALID_LINE_BUFFER_OPTIONS factory throw ValidationErrorContext unit
    INVALID_CONNECTION_OPTIONS factory throw ValidationErrorContext unit + integration
    SESSION_DISPOSED connect$ / send$ after dispose$ fatal undefined integration
    UNKNOWN unclassified dispose / disconnect fallback fatal { cause } unit
    Code Former emit location Notes
    INVALID_RECEIVE_REPLAY_OPTIONS factory Removed with options.receiveReplay
    RECEIVE_REPLAY_BUFFER_OVERFLOW receiveReplay$ overflow Removed with receiveReplay$

    See Migrating to v4 – Phase 2.

    Fatal vs non-fatal follows ERROR_SEVERITY inside reportError. Factory-thrown INVALID_* codes bypass reportError and throw directly to the caller.

    • [ ] Remove error handling for PORT_NOT_AVAILABLE / OPERATION_TIMEOUT (unreachable in v3.x).
    • [ ] Handle port acquisition failures with PORT_OPEN_FAILED / OPERATION_CANCELLED.
    • [ ] See API Reference – SerialError / SerialErrorCode for per-code emit conditions.

    Structured context for validation errors (INVALID_*) was added in #439. Use ValidationErrorContext (field, value, constraint, optional filterIndex) instead of parsing message.


    assertNever is a TypeScript utility for exhaustive switch checking. It was added as an internal exhaustiveness helper (#394 / PR #410) but was also exposed as a public export. Because it is not part of the Web Serial / SerialSession domain API, usage was audited (#440) and the results are recorded here and in the API Reference.

    Check Result
    package internal usage session-runtime.ts only (via assertNeverRuntime)
    examples usage none in apps/ or libs/
    documentation usage not listed in canonical exports (API_REFERENCE); not mentioned in migration docs
    export history added in Phase A (#394) as src/internal/assert-never.ts, re-exported from index.ts

    assertNever is an internal implementation utility, not a canonical public API. For exhaustive handling of SerialSessionState, prefer switch (state.status) with SerialSessionStatus, or narrowing with isConnectedSessionState.

    v3.x adds @deprecated annotations only; the public export is retained. Removal is deferred to the next major version.

    import { assertNever } from '@gurezo/web-serial-rxjs';

    session.state$.subscribe((state) => {
    switch (state.status) {
    case SerialSessionStatus.Connected:
    console.log(state.portInfo);
    break;
    default:
    assertNever(state);
    }
    });

    Cover all switch (state.status) cases, or use filter(isConnectedSessionState) in RxJS pipelines. If you need an exhaustiveness helper, define one locally in application code.

    import {
    SerialSessionStatus,
    isConnectedSessionState,
    type SerialSessionState,
    } from '@gurezo/web-serial-rxjs';

    function assertNever(value: never): never {
    throw new Error(`Unexpected value: ${String(value)}`);
    }

    session.state$.subscribe((state: SerialSessionState) => {
    switch (state.status) {
    case SerialSessionStatus.Connected:
    console.log(state.portInfo);
    break;
    case SerialSessionStatus.Idle:
    case SerialSessionStatus.Connecting:
    case SerialSessionStatus.Disconnecting:
    case SerialSessionStatus.Unsupported:
    case SerialSessionStatus.Error:
    case SerialSessionStatus.Disposed:
    break;
    default:
    assertNever(state);
    }
    });
    • [ ] Remove assertNever imports from @gurezo/web-serial-rxjs.
    • [ ] Define a local helper if you still need exhaustiveness checking.
    • [ ] Prefer switch (state.status) with SerialSessionStatus for SerialSessionState branches.
    • [ ] Migrate when TypeScript shows @deprecated warnings.

    assertNever remains available from the public export in v3.x. It is scheduled for removal in the next major version.


    SerialSessionOptions exposes W3C SerialOptions-derived connection fields and library-specific session feature options in a single type. As part of the TypeScript-first domain model consolidation, the public type surface and generated documentation were audited (#441).

    Check Result
    existing assignability Existing createSerialSession({ ... }) calls work without changes
    generated .d.ts public SerialConnectionOptions duplicated the internal SerialSessionConnectionFields Pick
    TypeDoc readability connection and feature fields appeared in one flat list; hierarchy showed an internal type name
    readonly input compatibility mutable arrays retained; readonly input assignability verified by regression tests
    examples libs/examples-shared already uses SerialConnectionOptions['baudRate']; example apps unchanged
    W3C SerialOptions drift detection connection fields remain a Pick from W3C types via SerialConnectionOptions

    Type safety was already sound, but conceptual separation improves documentation clarity. The canonical model is:

    SerialConnectionOptions     = W3C connection parameters for port.open
    SerialSessionFeatureOptions = library-specific session features
    SerialSessionOptions        = Partial<SerialConnectionOptions> & SerialSessionFeatureOptions
    
    • SerialConnectionOptionsbaudRate, dataBits, stopBits, parity, bufferSize, flowControl (passed to port.open)
    • SerialSessionFeatureOptionsfilters, terminalBuffer, lineBuffer (library-specific; receiveReplay was removed in v4 Phase 2 — see Migrating to v4)
    • SerialSessionOptions — composition of the two (factory argument)

    See API Reference – SerialSessionOptions for details. Boundary semantics (0 = unlimited for buffer limits only; connection fields require > 0) are documented there (#488).

    The createSerialSession(options?) signature and existing options object literals remain unchanged for connection / buffer fields. SerialSessionFeatureOptions is added as a new public export. receiveReplay is removed in v4 — migrate via Migrating to v4.