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);
}
});
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 (recommended — portInfo$ and getPortInfo() are deprecated).state.error when state.status === 'error' (same instance as errors$ for fatal errors).errors$ remains available.portInfo$ and getPortInfo() remain available in v3.x but are deprecated (see §5).isConnected$ remains available in v3.x but is deprecated (see §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$ deprecationSerialSession exposes both dispose$() and destroy$(). They are the same function — destroy$ is a legacy alias. Lifecycle terminology (dispose, disposed, SESSION_DISPOSED) already uses dispose$ as the canonical API.
destroy$() remains in v3.x for backward compatibility but is deprecated and scheduled for removal in the next major version.
session.destroy$().subscribe({
complete: () => console.log('session destroyed'),
});
session.dispose$().subscribe({
complete: () => console.log('session disposed'),
});
session.destroy$() with session.dispose$().@deprecated warnings by migrating to dispose$.dispose$ in new code and documentation.destroy$ remains available in v3.x and delegates to the same implementation as dispose$.portInfo$ / getPortInfo() deprecationv3.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() remain in v3.x for backward compatibility but are deprecated and scheduled for removal in the next major version. They expose SerialPortInfo | null, which does not encode the relationship between connection state and port information.
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.@deprecated warnings by migrating to the pattern above.state.portInfo in new code and documentation.portInfo$ and getPortInfo() remain available in v3.x.state.portInfo while connected.errors$ is not deprecated — it is an independent error event channel, not a duplicate of lifecycle state.isConnected$ deprecationv3.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$ is an Observable<boolean> that only projects whether the session is connected, so it loses the type information carried by the discriminated union. It remains in v3.x for backward compatibility but is deprecated and scheduled for removal in the next major version.
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(),
);
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.@deprecated warnings by migrating to the patterns above.state$ narrowing in new code and documentation.isConnected$ remains available in v3.x.state.status === SerialSessionStatus.Connected.state$ in framework adapters and examples.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.
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.
| 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 | 17 | Emitted at runtime in v3.x (or thrown at factory time) |
| Reserved | 2 | Present in the public API but not emitted in v3.x; scheduled for removal in the next major version |
| 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_RECEIVE_REPLAY_OPTIONS |
factory | throw | ValidationErrorContext |
unit + 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 |
RECEIVE_REPLAY_BUFFER_OVERFLOW |
receiveReplay$ overflow |
non-fatal | { maxChars, bufferSize } |
integration |
SESSION_DISPOSED |
connect$ / send$ after dispose$ |
fatal | undefined |
integration |
UNKNOWN |
unclassified dispose / disconnect fallback | fatal | { cause } |
unit |
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, receiveReplay, terminalBuffer, lineBuffer (library-specific)SerialSessionOptions — composition of the two (factory argument)See API Reference – SerialSessionOptions for details.
The createSerialSession(options?) signature and existing options object literals remain unchanged. SerialSessionFeatureOptions is added as a new public export.