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

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.
*/
static readonly BROWSER =
typeof globalThis.navigator !== "undefined" &&
!!globalThis.navigator.usb
static readonly BROWSER = /* #__PURE__ */ (() => {
typeof globalThis.navigator !== "undefined" && globalThis.navigator.usb
? new AdbDaemonWebUsbDeviceManager(globalThis.navigator.usb)
: undefined;
})();
#usbManager: USB;

View file

@ -15,16 +15,27 @@ import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
import { AdbBanner } from "../banner.js";
import type { DeviceObserver as DeviceObserverBase } from "../device-observer.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 { AdbServerStream, FAIL } from "./stream.js";
import { AdbServerStream } from "./stream.js";
import { AdbServerTransport } from "./transport.js";
/**
* Client for the ADB Server.
*/
export class AdbServerClient {
static NetworkError = _NetworkError;
static UnauthorizedError = _UnauthorizedError;
static AlreadyConnectedError = _AlreadyConnectedError;
static parseDeviceList(value: string): AdbServerClient.Device[] {
const devices: AdbServerClient.Device[] = [];
for (const line of value.split("\n")) {
@ -99,8 +110,8 @@ export class AdbServerClient {
readonly connector: AdbServerClient.ServerConnector;
readonly wireless = new AdbServerClient.WirelessCommands(this);
readonly mDns = new AdbServerClient.MDnsCommands(this);
readonly wireless = new WirelessCommands(this);
readonly mDns = new MDnsCommands(this);
#observerOwner = new AdbServerDeviceObserverOwner(this);
constructor(connector: AdbServerClient.ServerConnector) {
@ -537,138 +548,11 @@ export namespace AdbServerClient {
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> {
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,26 +10,27 @@ import { AdbFeature } from "../features.js";
import type { AdbServerClient } from "./client.js";
export const ADB_SERVER_DEFAULT_FEATURES = [
AdbFeature.ShellV2,
AdbFeature.Cmd,
AdbFeature.StatV2,
AdbFeature.ListV2,
AdbFeature.FixedPushMkdir,
"apex",
AdbFeature.Abb,
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
// No special handling required.
"fixed_push_symlink_timestamp",
AdbFeature.AbbExec,
"remount_shell",
"track_app",
AdbFeature.SendReceiveV2,
"sendrecv_v2_brotli",
"sendrecv_v2_lz4",
"sendrecv_v2_zstd",
"sendrecv_v2_dry_run_send",
] as AdbFeature[];
export const ADB_SERVER_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
[
AdbFeature.ShellV2,
AdbFeature.Cmd,
AdbFeature.StatV2,
AdbFeature.ListV2,
AdbFeature.FixedPushMkdir,
"apex",
AdbFeature.Abb,
// only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
// No special handling required.
"fixed_push_symlink_timestamp",
AdbFeature.AbbExec,
"remount_shell",
"track_app",
AdbFeature.SendReceiveV2,
"sendrecv_v2_brotli",
"sendrecv_v2_lz4",
"sendrecv_v2_zstd",
"sendrecv_v2_dry_run_send",
] as AdbFeature[])();
export class AdbServerTransport implements AdbTransport {
#client: AdbServerClient;

View file

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

View file

@ -42,14 +42,16 @@ export interface BufferLike {
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:
| string
| number
| Field<number, never, unknown>
| BufferLengthConverter<string, unknown>,
converter?: Converter<Uint8Array, unknown>,
): Field<unknown, string, Record<string, unknown>> {
): Field<unknown, string, Record<string, unknown>> => {
if (typeof lengthOrField === "number") {
if (converter) {
if (lengthOrField === 0) {
@ -257,4 +259,4 @@ export const buffer: BufferLike = function (
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__ */
function factory<T>(
fn: NumberField<T>,
size: number,
serialize: Field<T, never, never>["serialize"],
deserialize: Field<T, never, never>["deserialize"],
): NumberField<T> {
const result = () => result;
result.size = size;
result.serialize = serialize;
result.deserialize = deserialize;
return result as never;
) {
fn.size = size;
fn.serialize = serialize;
fn.deserialize = deserialize;
}
export const u8 = factory<number>(
export const u8: NumberField<number> = (() => u8) as never;
factory(
u8,
1,
(value, { buffer, index }) => {
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,
(value, { buffer, index }) => {
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,
(value, { buffer, index, 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,
(value, { buffer, index, 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,
(value, { buffer, index, 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,
(value, { buffer, index, 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,
(value, { buffer, index, 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,
(value, { buffer, index, littleEndian }) => {
setInt64(buffer, index, value, littleEndian);

View file

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