Skip to content

Commit 9dd8265

Browse files
committed
Import classes from other libs
1 parent 8920a80 commit 9dd8265

13 files changed

Lines changed: 661 additions & 22 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web-device-mux",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "A small library for managing connections to WebUSB, WebHID, and Web Serial devices from multiple tabs at the same time.",
55
"type": "module",
66
"repository": {

src/Channel.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { IDeviceInformation } from "./Device.js";
2+
import type { DeviceCommunicationError } from "./Error.js";
3+
4+
/** Possible ways to communicate with a device */
5+
export type DeviceChannelType
6+
= "USB"
7+
| "Serial"
8+
| "Bluetooth"
9+
| "Network"
10+
11+
/** Whether data can be transmitted or received from the device. */
12+
export enum ConnectionDirectionMode {
13+
none,
14+
unidirectional,
15+
bidirectional
16+
}
17+
18+
/** Behavior options when communicating with a device. */
19+
export interface IDeviceCommunicationOptions {
20+
/** Whether to display printer communication to the dev console. */
21+
debug: boolean;
22+
23+
/** Milliseconds to wait for messages from a device before assuming it's done talking. Defaults to 500ms. */
24+
messageWaitTimeoutMS?: number
25+
}
26+
27+
/** A communication channel for talking to a device. */
28+
export interface IDeviceChannel<TOutput, TInput> {
29+
/** Gets the mode the communication is set up as. */
30+
get commMode(): ConnectionDirectionMode;
31+
32+
/** Gets this channel type. */
33+
readonly channelType: DeviceChannelType;
34+
35+
/** A promise indicating this communication channel is ready for use. */
36+
get ready(): Promise<boolean>;
37+
38+
/** Whether the device is connected. */
39+
get connected(): boolean;
40+
41+
/** Close the channel, disallowing future communication. */
42+
dispose(): Promise<void>;
43+
44+
/** Gets the basic information for the device connected on this channel. */
45+
getDeviceInfo(): IDeviceInformation
46+
47+
/**
48+
* Send a series of commands to the device.
49+
* @param commandBuffer The series of commands to send in order.
50+
*/
51+
sendCommands(commandBuffer: TOutput): Promise<DeviceCommunicationError | undefined>;
52+
53+
/** Request data from the device. */
54+
getInput(): Promise<TInput[] | DeviceCommunicationError>;
55+
}

src/Device.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
/** Interface describing a device that can be connected to. */
3+
export interface IDevice {
4+
/** Close the connection to this device and clean up unmanaged resources. */
5+
dispose(): Promise<void>;
6+
7+
/** Whether the device is connected. */
8+
get connected(): boolean;
9+
10+
/** A promise indicating this device is ready to be used. */
11+
ready: Promise<boolean>;
12+
}
13+
14+
/** An event object corresponding to a particular device. */
15+
export interface IDeviceEvent<TDevice extends IDevice> {
16+
/** The device the event is for. */
17+
device: TDevice;
18+
}
19+
20+
/** Basic metadata about a device, including from before we connect to it. */
21+
export interface IDeviceInformation {
22+
/** A string representing the manufacturer, if available. */
23+
readonly manufacturerName?: string | undefined;
24+
/** A string representing the device product name, if available. */
25+
readonly productName?: string | undefined;
26+
/** A string representing the device's unique serial number, if available. */
27+
readonly serialNumber?: string | undefined;
28+
}

src/Error.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/** Exception thrown from the WebDeviceMux library. */
2+
export class WebDeviceMuxError extends Error {
3+
constructor(message: string) {
4+
super(message);
5+
this.name = this.constructor.name;
6+
}
7+
}
8+
9+
/** Error indicating communication with the device could not be initiated. */
10+
export class DeviceConnectionError extends WebDeviceMuxError {
11+
constructor(message?: string, innerException?: Error) {
12+
super(message ?? innerException?.message ?? 'Error connecting to device');
13+
this.innerException = innerException;
14+
}
15+
16+
innerException?: Error;
17+
}
18+
19+
/** Error indicating communication with the device failed. */
20+
export class DeviceCommunicationError extends WebDeviceMuxError {
21+
constructor(message?: string, innerException?: Error) {
22+
super(message ?? innerException?.message ?? 'Error communicating with device');
23+
this.innerException = innerException;
24+
}
25+
26+
innerException?: Error;
27+
}
28+
29+
/** Error indicating the device was not ready to communicate. */
30+
export class DeviceNotReadyError extends DeviceCommunicationError {
31+
constructor(message?: string, innerException?: Error) {
32+
super(message ?? innerException?.message ?? 'Device not ready to communicate.');
33+
}
34+
}

src/InputListener.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// TODO: Rewrite the entire message handling system as a stream transformer?
2+
3+
import { DeviceCommunicationError } from "./Error.js";
4+
5+
export type DataProvider<TInput> = () => Promise<TInput[] | DeviceCommunicationError>;
6+
export type InputHandler<TInput> = (input: TInput[]) => IHandlerResponse<TInput>;
7+
export interface IHandlerResponse<TInput> {
8+
remainderData?: TInput[];
9+
}
10+
11+
export class InputMessageListener<TInput> {
12+
public start() {
13+
// We intentionally want to kick off an async promise here without needing
14+
// the calling function to be async. The promise we're starting is meant to
15+
// run forever and never resolve until it's being shut down.
16+
// eslint-disable-next-line no-async-promise-executor
17+
new Promise<void>(async (_, reject) => {
18+
let aggregate: TInput[] = [];
19+
do {
20+
const data = await this._dataProvider();
21+
22+
if (data instanceof DeviceCommunicationError) {
23+
console.error(`Error getting data from source: `, data);
24+
reject(data);
25+
break;
26+
}
27+
28+
if (data.length === 0) {
29+
continue;
30+
}
31+
32+
aggregate.push(...data);
33+
this.logIfDebug(`Got data from provider, now has ${aggregate.length} items in receive buffer. Data: `, data);
34+
35+
// The handler determines if what we got was sufficient, or if we need more.
36+
const handleResult = this._inputHandler(aggregate);
37+
if (handleResult.remainderData !== undefined && handleResult.remainderData.length !== 0) {
38+
this.logIfDebug(`Input handler provided a ${handleResult.remainderData.length} length incomplete buffer.`);
39+
}
40+
41+
// The handler is not required to be stateful, this is instead.
42+
// The handler may indicate more data is expected by returning incomplete
43+
// data back. We prefix our buffer with that incomplete data to add more
44+
// to it and wait again for more.
45+
aggregate = handleResult.remainderData ?? [];
46+
} while (!this._disposed);
47+
})
48+
.catch((reason) => {
49+
// TODO: Better logging?
50+
this.logIfDebug(`Device stream listener stopped listening unexpectedly.`, reason);
51+
});
52+
}
53+
54+
private _disposed = false;
55+
public dispose() {
56+
if (this._disposed) { return; }
57+
this._disposed = true;
58+
}
59+
60+
constructor(
61+
private readonly _dataProvider: DataProvider<TInput>,
62+
private readonly _inputHandler: InputHandler<TInput>,
63+
private readonly _debugLogging: boolean = true, // TODO: false
64+
) {}
65+
66+
private logIfDebug(...obj: unknown[]) {
67+
if (this._debugLogging) {
68+
console.debug(...obj);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)