v3 introduces two TypeScript-facing breaking changes:
SerialErrorCode — enum → const object + union type (runtime values unchanged).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 |
SerialErrorCode const object| 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_FAILEDerror.is(SerialErrorCode.LINE_BUFFER_OVERFLOW) with narrowed contextswitch (error.code) { case SerialErrorCode.READ_FAILED: ... }import type { SerialErrorCode } from '@gurezo/web-serial-rxjs'.enums/SerialErrorCode.html to variables/SerialErrorCode.html..d.ts — declaration shape changes from enum to const + type alias.state$| 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 };
import { SerialSessionState } used as constants with SerialSessionStatus.state === SerialSessionState.X with state.status === SerialSessionStatus.X.switch (state) with switch (state.status) (or compare state.status in if).state.portInfo when state.status === SerialSessionStatus.Connected (portInfo$ and getPortInfo() were removed — see §5).state.error when state.status === 'error' (same instance as errors$ for fatal errors).errors$ remains available as the independent error event channel.portInfo$, getPortInfo(), isConnected$, destroy$()) are removed — migrate with §4–§6.originalError deprecationv3.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);
}
});
error.originalError with error.context.cause (narrow with error.is(code) first).new SerialError(code, message, cause), switch to new SerialError(code, message, undefined, { cause }).@deprecated warnings by migrating to the patterns above.originalError remains available in v3.x.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$() removaldestroy$() 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'),
});
session.destroy$() with session.dispose$().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.
portInfo$ / getPortInfo() removalv3.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);
}
});
portInfo$ subscriptions with state$ and read state.portInfo when state.status === SerialSessionStatus.Connected.getPortInfo() with state$ narrowing and state.portInfo.state.portInfo in new code and documentation.errors$ is not a duplicate of lifecycle state — it remains the independent error event channel.isConnected$ removalv3.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
}
});
state$ narrowing)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.
filter with connected-state narrowingWhen 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,
);
isConnected$ subscriptions with state$ and narrow on state.status === SerialSessionStatus.Connected.state$ with map or computed.state$ narrowing in new code and documentation.getCurrentPort() removalSerialSession.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.
getCurrentPort() calls.state$ narrowed on SerialSessionStatus.Connected and read state.portInfo for device identification.SerialErrorCode runtime emission auditSome 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.
PORT_NOT_AVAILABLE / OPERATION_TIMEOUT (unreachable in v3.x).PORT_OPEN_FAILED / OPERATION_CANCELLED.Structured context for validation errors (INVALID_*) was added in #439. Use ValidationErrorContext (field, value, constraint, optional filterIndex) instead of parsing message.
assertNever public export auditassertNever 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);
}
});
assertNever imports from @gurezo/web-serial-rxjs.switch (state.status) with SerialSessionStatus for SerialSessionState branches.@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
SerialConnectionOptions — baudRate, dataBits, stopBits, parity, bufferSize, flowControl (passed to port.open)SerialSessionFeatureOptions — filters, 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.