refactor: remove side effects

This commit is contained in:
Simon Chan 2025-03-13 17:31:51 +08:00
parent a335c1495c
commit 92511c63de
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
12 changed files with 241 additions and 194 deletions

View file

@ -22,6 +22,7 @@ import {
import type { ExactReadable } from "@yume-chan/struct"; import type { ExactReadable } from "@yume-chan/struct";
import { EmptyUint8Array } from "@yume-chan/struct"; import { EmptyUint8Array } from "@yume-chan/struct";
import { DeviceBusyError as _DeviceBusyError } from "./error.js";
import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js"; import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js";
import { findUsbEndpoints, getSerialNumber, isErrorName } from "./utils.js"; import { findUsbEndpoints, getSerialNumber, isErrorName } from "./utils.js";
@ -254,6 +255,8 @@ export class AdbDaemonWebUsbConnection
} }
export class AdbDaemonWebUsbDevice implements AdbDaemonDevice { export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
static DeviceBusyError = _DeviceBusyError;
#interface: UsbInterfaceIdentifier; #interface: UsbInterfaceIdentifier;
#usbManager: USB; #usbManager: USB;
@ -348,11 +351,5 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
} }
export namespace AdbDaemonWebUsbDevice { export namespace AdbDaemonWebUsbDevice {
export class DeviceBusyError extends Error { export type DeviceBusyError = _DeviceBusyError;
constructor(cause?: Error) {
super("The device is already in used by another program", {
cause,
});
}
}
} }

View file

@ -0,0 +1,7 @@
export class DeviceBusyError extends Error {
constructor(cause?: Error) {
super("The device is already in used by another program", {
cause,
});
}
}

View file

@ -18,11 +18,11 @@ export class AdbDaemonWebUsbDeviceManager {
* *
* May be `undefined` if current runtime does not support WebUSB. * May be `undefined` if current runtime does not support WebUSB.
*/ */
static readonly BROWSER = static readonly BROWSER = /* #__PURE__ */ (() => {
typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator !== "undefined" && globalThis.navigator.usb
!!globalThis.navigator.usb
? new AdbDaemonWebUsbDeviceManager(globalThis.navigator.usb) ? new AdbDaemonWebUsbDeviceManager(globalThis.navigator.usb)
: undefined; : undefined;
})();
#usbManager: USB; #usbManager: USB;

View file

@ -15,16 +15,27 @@ import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
import { AdbBanner } from "../banner.js"; import { AdbBanner } from "../banner.js";
import type { DeviceObserver as DeviceObserverBase } from "../device-observer.js"; import type { DeviceObserver as DeviceObserverBase } from "../device-observer.js";
import type { AdbFeature } from "../features.js"; import type { AdbFeature } from "../features.js";
import { hexToNumber, sequenceEqual } from "../utils/index.js"; import { hexToNumber } from "../utils/index.js";
import {
MDnsCommands,
WirelessCommands,
AlreadyConnectedError as _AlreadyConnectedError,
NetworkError as _NetworkError,
UnauthorizedError as _UnauthorizedError,
} from "./commands/index.js";
import { AdbServerDeviceObserverOwner } from "./observer.js"; import { AdbServerDeviceObserverOwner } from "./observer.js";
import { AdbServerStream, FAIL } from "./stream.js"; import { AdbServerStream } from "./stream.js";
import { AdbServerTransport } from "./transport.js"; import { AdbServerTransport } from "./transport.js";
/** /**
* Client for the ADB Server. * Client for the ADB Server.
*/ */
export class AdbServerClient { export class AdbServerClient {
static NetworkError = _NetworkError;
static UnauthorizedError = _UnauthorizedError;
static AlreadyConnectedError = _AlreadyConnectedError;
static parseDeviceList(value: string): AdbServerClient.Device[] { static parseDeviceList(value: string): AdbServerClient.Device[] {
const devices: AdbServerClient.Device[] = []; const devices: AdbServerClient.Device[] = [];
for (const line of value.split("\n")) { for (const line of value.split("\n")) {
@ -99,8 +110,8 @@ export class AdbServerClient {
readonly connector: AdbServerClient.ServerConnector; readonly connector: AdbServerClient.ServerConnector;
readonly wireless = new AdbServerClient.WirelessCommands(this); readonly wireless = new WirelessCommands(this);
readonly mDns = new AdbServerClient.MDnsCommands(this); readonly mDns = new MDnsCommands(this);
#observerOwner = new AdbServerDeviceObserverOwner(this); #observerOwner = new AdbServerDeviceObserverOwner(this);
constructor(connector: AdbServerClient.ServerConnector) { constructor(connector: AdbServerClient.ServerConnector) {
@ -537,138 +548,11 @@ export namespace AdbServerClient {
transportId: bigint; transportId: bigint;
} }
export class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = "NetworkError";
}
}
export class UnauthorizedError extends Error {
constructor(message: string) {
super(message);
this.name = "UnauthorizedError";
}
}
export class AlreadyConnectedError extends Error {
constructor(message: string) {
super(message);
this.name = "AlreadyConnectedError";
}
}
export class WirelessCommands {
#client: AdbServerClient;
constructor(client: AdbServerClient) {
this.#client = client;
}
/**
* `adb pair <password> <address>`
*/
async pair(address: string, password: string): Promise<void> {
const connection = await this.#client.createConnection(
`host:pair:${password}:${address}`,
);
try {
const response = await connection.readExactly(4);
// `response` is either `FAIL`, or 4 hex digits for length of the string
if (sequenceEqual(response, FAIL)) {
throw new Error(await connection.readString());
}
const length = hexToNumber(response);
// Ignore the string as it's always `Successful ...`
await connection.readExactly(length);
} finally {
await connection.dispose();
}
}
/**
* `adb connect <address>`
*/
async connect(address: string): Promise<void> {
const connection = await this.#client.createConnection(
`host:connect:${address}`,
);
try {
const response = await connection.readString();
switch (response) {
case `already connected to ${address}`:
throw new AdbServerClient.AlreadyConnectedError(
response,
);
case `failed to connect to ${address}`: // `adb pair` mode not authorized
case `failed to authenticate to ${address}`: // `adb tcpip` mode not authorized
throw new AdbServerClient.UnauthorizedError(response);
case `connected to ${address}`:
return;
default:
throw new AdbServerClient.NetworkError(response);
}
} finally {
await connection.dispose();
}
}
/**
* `adb disconnect <address>`
*/
async disconnect(address: string): Promise<void> {
const connection = await this.#client.createConnection(
`host:disconnect:${address}`,
);
try {
await connection.readString();
} finally {
await connection.dispose();
}
}
}
export class MDnsCommands {
#client: AdbServerClient;
constructor(client: AdbServerClient) {
this.#client = client;
}
async check() {
const connection =
await this.#client.createConnection("host:mdns:check");
try {
const response = await connection.readString();
return !response.startsWith("ERROR:");
} finally {
await connection.dispose();
}
}
async getServices() {
const connection =
await this.#client.createConnection("host:mdns:services");
try {
const response = await connection.readString();
return response
.split("\n")
.filter(Boolean)
.map((line) => {
const parts = line.split("\t");
return {
name: parts[0]!,
service: parts[1]!,
address: parts[2]!,
};
});
} finally {
await connection.dispose();
}
}
}
export interface DeviceObserver extends DeviceObserverBase<Device> { export interface DeviceObserver extends DeviceObserverBase<Device> {
onError: Event<Error>; onError: Event<Error>;
} }
export type NetworkError = _NetworkError;
export type UnauthorizedError = _UnauthorizedError;
export type AlreadyConnectedError = _AlreadyConnectedError;
} }

View file

@ -0,0 +1,2 @@
export * from "./m-dns.js";
export * from "./wireless.js";

View file

@ -0,0 +1,43 @@
// cspell:ignore mdns
import type { AdbServerClient } from "../client.js";
export class MDnsCommands {
#client: AdbServerClient;
constructor(client: AdbServerClient) {
this.#client = client;
}
async check() {
const connection =
await this.#client.createConnection("host:mdns:check");
try {
const response = await connection.readString();
return !response.startsWith("ERROR:");
} finally {
await connection.dispose();
}
}
async getServices() {
const connection =
await this.#client.createConnection("host:mdns:services");
try {
const response = await connection.readString();
return response
.split("\n")
.filter(Boolean)
.map((line) => {
const parts = line.split("\t");
return {
name: parts[0]!,
service: parts[1]!,
address: parts[2]!,
};
});
} finally {
await connection.dispose();
}
}
}

View file

@ -0,0 +1,95 @@
// cspell:ignore tport
import { hexToNumber, sequenceEqual } from "../../utils/index.js";
import type { AdbServerClient } from "../client.js";
import { FAIL } from "../stream.js";
export class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = "NetworkError";
}
}
export class UnauthorizedError extends Error {
constructor(message: string) {
super(message);
this.name = "UnauthorizedError";
}
}
export class AlreadyConnectedError extends Error {
constructor(message: string) {
super(message);
this.name = "AlreadyConnectedError";
}
}
export class WirelessCommands {
#client: AdbServerClient;
constructor(client: AdbServerClient) {
this.#client = client;
}
/**
* `adb pair <password> <address>`
*/
async pair(address: string, password: string): Promise<void> {
const connection = await this.#client.createConnection(
`host:pair:${password}:${address}`,
);
try {
const response = await connection.readExactly(4);
// `response` is either `FAIL`, or 4 hex digits for length of the string
if (sequenceEqual(response, FAIL)) {
throw new Error(await connection.readString());
}
const length = hexToNumber(response);
// Ignore the string as it's always `Successful ...`
await connection.readExactly(length);
} finally {
await connection.dispose();
}
}
/**
* `adb connect <address>`
*/
async connect(address: string): Promise<void> {
const connection = await this.#client.createConnection(
`host:connect:${address}`,
);
try {
const response = await connection.readString();
switch (response) {
case `already connected to ${address}`:
throw new AlreadyConnectedError(response);
case `failed to connect to ${address}`: // `adb pair` mode not authorized
case `failed to authenticate to ${address}`: // `adb tcpip` mode not authorized
throw new UnauthorizedError(response);
case `connected to ${address}`:
return;
default:
throw new NetworkError(response);
}
} finally {
await connection.dispose();
}
}
/**
* `adb disconnect <address>`
*/
async disconnect(address: string): Promise<void> {
const connection = await this.#client.createConnection(
`host:disconnect:${address}`,
);
try {
await connection.readString();
} finally {
await connection.dispose();
}
}
}

View file

@ -10,7 +10,8 @@ import { AdbFeature } from "../features.js";
import type { AdbServerClient } from "./client.js"; import type { AdbServerClient } from "./client.js";
export const ADB_SERVER_DEFAULT_FEATURES = [ export const ADB_SERVER_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
[
AdbFeature.ShellV2, AdbFeature.ShellV2,
AdbFeature.Cmd, AdbFeature.Cmd,
AdbFeature.StatV2, AdbFeature.StatV2,
@ -29,7 +30,7 @@ export const ADB_SERVER_DEFAULT_FEATURES = [
"sendrecv_v2_lz4", "sendrecv_v2_lz4",
"sendrecv_v2_zstd", "sendrecv_v2_zstd",
"sendrecv_v2_dry_run_send", "sendrecv_v2_dry_run_send",
] as AdbFeature[]; ] as AdbFeature[])();
export class AdbServerTransport implements AdbTransport { export class AdbServerTransport implements AdbTransport {
#client: AdbServerClient; #client: AdbServerClient;

View file

@ -6,11 +6,14 @@ import type { SingleUser } from "./utils.js";
export type SettingsNamespace = "system" | "secure" | "global"; export type SettingsNamespace = "system" | "secure" | "global";
export enum SettingsResetMode { export const SettingsResetMode = {
UntrustedDefaults = "untrusted_defaults", UntrustedDefaults: "untrusted_defaults",
UntrustedClear = "untrusted_clear", UntrustedClear: "untrusted_clear",
TrustedDefaults = "trusted_defaults", TrustedDefaults: "trusted_defaults",
} } as const;
export type SettingsResetMode =
(typeof SettingsResetMode)[keyof typeof SettingsResetMode];
export interface SettingsOptions { export interface SettingsOptions {
user?: SingleUser; user?: SingleUser;

View file

@ -42,14 +42,16 @@ export interface BufferLike {
export const EmptyUint8Array = new Uint8Array(0); export const EmptyUint8Array = new Uint8Array(0);
export const buffer: BufferLike = function ( // Prettier will move the annotation and make it invalid
// prettier-ignore
export const buffer: BufferLike = (/* #__NO_SIDE_EFFECTS__ */ (
lengthOrField: lengthOrField:
| string | string
| number | number
| Field<number, never, unknown> | Field<number, never, unknown>
| BufferLengthConverter<string, unknown>, | BufferLengthConverter<string, unknown>,
converter?: Converter<Uint8Array, unknown>, converter?: Converter<Uint8Array, unknown>,
): Field<unknown, string, Record<string, unknown>> { ): Field<unknown, string, Record<string, unknown>> => {
if (typeof lengthOrField === "number") { if (typeof lengthOrField === "number") {
if (converter) { if (converter) {
if (lengthOrField === 0) { if (lengthOrField === 0) {
@ -257,4 +259,4 @@ export const buffer: BufferLike = function (
return reader.readExactly(length); return reader.readExactly(length);
}, },
}; };
} as never; }) as never;

View file

@ -23,18 +23,19 @@ export interface NumberField<T> extends Field<T, never, never> {
/* #__NO_SIDE_EFFECTS__ */ /* #__NO_SIDE_EFFECTS__ */
function factory<T>( function factory<T>(
fn: NumberField<T>,
size: number, size: number,
serialize: Field<T, never, never>["serialize"], serialize: Field<T, never, never>["serialize"],
deserialize: Field<T, never, never>["deserialize"], deserialize: Field<T, never, never>["deserialize"],
): NumberField<T> { ) {
const result = () => result; fn.size = size;
result.size = size; fn.serialize = serialize;
result.serialize = serialize; fn.deserialize = deserialize;
result.deserialize = deserialize;
return result as never;
} }
export const u8 = factory<number>( export const u8: NumberField<number> = (() => u8) as never;
factory(
u8,
1, 1,
(value, { buffer, index }) => { (value, { buffer, index }) => {
buffer[index] = value; buffer[index] = value;
@ -45,7 +46,9 @@ export const u8 = factory<number>(
}), }),
); );
export const s8 = factory<number>( export const s8: NumberField<number> = (() => s8) as never;
factory(
s8,
1, 1,
(value, { buffer, index }) => { (value, { buffer, index }) => {
buffer[index] = value; buffer[index] = value;
@ -56,7 +59,9 @@ export const s8 = factory<number>(
}), }),
); );
export const u16 = factory<number>( export const u16: NumberField<number> = (() => u16) as never;
factory(
u16,
2, 2,
(value, { buffer, index, littleEndian }) => { (value, { buffer, index, littleEndian }) => {
setUint16(buffer, index, value, littleEndian); setUint16(buffer, index, value, littleEndian);
@ -67,7 +72,9 @@ export const u16 = factory<number>(
}), }),
); );
export const s16 = factory<number>( export const s16: NumberField<number> = (() => u16) as never;
factory(
s16,
2, 2,
(value, { buffer, index, littleEndian }) => { (value, { buffer, index, littleEndian }) => {
setInt16(buffer, index, value, littleEndian); setInt16(buffer, index, value, littleEndian);
@ -78,7 +85,9 @@ export const s16 = factory<number>(
}), }),
); );
export const u32 = factory<number>( export const u32: NumberField<number> = (() => u32) as never;
factory(
u32,
4, 4,
(value, { buffer, index, littleEndian }) => { (value, { buffer, index, littleEndian }) => {
setUint32(buffer, index, value, littleEndian); setUint32(buffer, index, value, littleEndian);
@ -89,7 +98,9 @@ export const u32 = factory<number>(
}), }),
); );
export const s32 = factory<number>( export const s32: NumberField<number> = (() => s32) as never;
factory(
s32,
4, 4,
(value, { buffer, index, littleEndian }) => { (value, { buffer, index, littleEndian }) => {
setInt32(buffer, index, value, littleEndian); setInt32(buffer, index, value, littleEndian);
@ -100,7 +111,9 @@ export const s32 = factory<number>(
}), }),
); );
export const u64 = factory<bigint>( export const u64: NumberField<bigint> = (() => u64) as never;
factory(
u64,
8, 8,
(value, { buffer, index, littleEndian }) => { (value, { buffer, index, littleEndian }) => {
setUint64(buffer, index, value, littleEndian); setUint64(buffer, index, value, littleEndian);
@ -111,7 +124,9 @@ export const u64 = factory<bigint>(
}), }),
); );
export const s64 = factory<bigint>( export const s64: NumberField<bigint> = (() => u64) as never;
factory(
s64,
8, 8,
(value, { buffer, index, littleEndian }) => { (value, { buffer, index, littleEndian }) => {
setInt64(buffer, index, value, littleEndian); setInt64(buffer, index, value, littleEndian);

View file

@ -25,19 +25,17 @@ export interface String {
): Field<string, KOmitInit, KS>; ): Field<string, KOmitInit, KS>;
} }
// Rollup doesn't support `/* #__NO_SIDE_EFFECTS__ */ export const a = () => {} // Prettier will move the annotation and make it invalid
/* #__NO_SIDE_EFFECTS__ */ // prettier-ignore
function _string( export const string: String = (/* #__NO_SIDE_EFFECTS__ */ (
lengthOrField: string | number | BufferLengthConverter<string, unknown>, lengthOrField: string | number | BufferLengthConverter<string, unknown>,
): Field<string, string, Record<string, unknown>> & { ): Field<string, string, Record<string, unknown>> & {
as: <T>(infer: T) => Field<T, string, Record<string, unknown>>; as: <T>(infer: T) => Field<T, string, Record<string, unknown>>;
} { } => {
const field = buffer(lengthOrField as never, { const field = buffer(lengthOrField as never, {
convert: decodeUtf8, convert: decodeUtf8,
back: encodeUtf8, back: encodeUtf8,
}); });
(field as never as { as: unknown }).as = () => field; (field as never as { as: unknown }).as = () => field;
return field as never; return field as never;
} }) as never;
export const string: String = _string as never;