Loading .changeset/five-glasses-attend.md 0 → 100644 +7 −0 Original line number Diff line number Diff line --- "@yume-chan/adb-daemon-webusb": minor "@yume-chan/event": minor "@yume-chan/adb": minor --- Make `DeviceObserver#onListChange` sticky libraries/adb-daemon-webusb/src/observer.ts +26 −10 Original line number Diff line number Diff line import type { DeviceObserver } from "@yume-chan/adb"; import { unorderedRemove } from "@yume-chan/adb"; import { EventEmitter, StickyEventEmitter } from "@yume-chan/event"; import { Loading @@ -24,20 +25,26 @@ export class AdbDaemonWebUsbDeviceObserver return new AdbDaemonWebUsbDeviceObserver(usb, devices, options); } #filters: (USBDeviceFilter & UsbInterfaceFilter)[]; #exclusionFilters?: USBDeviceFilter[] | undefined; #usbManager: USB; readonly #filters: (USBDeviceFilter & UsbInterfaceFilter)[]; readonly #exclusionFilters?: USBDeviceFilter[] | undefined; readonly #usbManager: USB; #onDeviceAdd = new EventEmitter<AdbDaemonWebUsbDevice[]>(); readonly #onDeviceAdd = new EventEmitter< readonly AdbDaemonWebUsbDevice[] >(); onDeviceAdd = this.#onDeviceAdd.event; #onDeviceRemove = new EventEmitter<AdbDaemonWebUsbDevice[]>(); readonly #onDeviceRemove = new EventEmitter< readonly AdbDaemonWebUsbDevice[] >(); onDeviceRemove = this.#onDeviceRemove.event; #onListChange = new StickyEventEmitter<AdbDaemonWebUsbDevice[]>(); readonly #onListChange = new StickyEventEmitter< readonly AdbDaemonWebUsbDevice[] >(); onListChange = this.#onListChange.event; current: AdbDaemonWebUsbDevice[] = []; current: readonly AdbDaemonWebUsbDevice[] = []; constructor( usb: USB, Loading @@ -47,9 +54,12 @@ export class AdbDaemonWebUsbDeviceObserver this.#filters = mergeDefaultAdbInterfaceFilter(options.filters); this.#exclusionFilters = options.exclusionFilters; this.#usbManager = usb; this.current = initial .map((device) => this.#convertDevice(device)) .filter((device) => !!device); // Fire `onListChange` to set the sticky value this.#onListChange.fire(this.current); this.#usbManager.addEventListener("connect", this.#handleConnect); this.#usbManager.addEventListener("disconnect", this.#handleDisconnect); Loading @@ -74,8 +84,11 @@ export class AdbDaemonWebUsbDeviceObserver return; } const next = this.current.slice(); next.push(device); this.current = next; this.#onDeviceAdd.fire([device]); this.current.push(device); this.#onListChange.fire(this.current); }; Loading @@ -85,9 +98,12 @@ export class AdbDaemonWebUsbDeviceObserver ); if (index !== -1) { const device = this.current[index]!; const next = this.current.slice(); unorderedRemove(next, index); this.current = next; this.#onDeviceRemove.fire([device]); this.current[index] = this.current[this.current.length - 1]!; this.current.length -= 1; this.#onListChange.fire(this.current); } }; Loading libraries/adb/src/device-observer.ts +4 −4 Original line number Diff line number Diff line Loading @@ -2,9 +2,9 @@ import type { MaybePromiseLike } from "@yume-chan/async"; import type { Event } from "@yume-chan/event"; export interface DeviceObserver<T> { onDeviceAdd: Event<T[]>; onDeviceRemove: Event<T[]>; onListChange: Event<T[]>; current: T[]; readonly onDeviceAdd: Event<readonly T[]>; readonly onDeviceRemove: Event<readonly T[]>; readonly onListChange: Event<readonly T[]>; readonly current: readonly T[]; stop(): MaybePromiseLike<void>; } libraries/adb/src/server/observer.ts +89 −64 Original line number Diff line number Diff line Loading @@ -5,20 +5,23 @@ import { Ref } from "../utils/index.js"; import { AdbServerClient } from "./client.js"; import type { AdbServerStream } from "./stream.js"; function unorderedRemove<T>(array: T[], index: number) { export function unorderedRemove<T>(array: T[], index: number) { if (index < 0 || index >= array.length) { return; } array[index] = array[array.length - 1]!; array.length -= 1; } export class AdbServerDeviceObserverOwner { current: AdbServerClient.Device[] = []; current: readonly AdbServerClient.Device[] = []; #client: AdbServerClient; readonly #client: AdbServerClient; #stream: Promise<AdbServerStream> | undefined; #observers: { onDeviceAdd: EventEmitter<AdbServerClient.Device[]>; onDeviceRemove: EventEmitter<AdbServerClient.Device[]>; onListChange: EventEmitter<AdbServerClient.Device[]>; onDeviceAdd: EventEmitter<readonly AdbServerClient.Device[]>; onDeviceRemove: EventEmitter<readonly AdbServerClient.Device[]>; onListChange: EventEmitter<readonly AdbServerClient.Device[]>; onError: EventEmitter<Error>; }[] = []; Loading @@ -27,42 +30,50 @@ export class AdbServerDeviceObserverOwner { } async #receive(stream: AdbServerStream) { try { while (true) { const response = await stream.readString(); const next = AdbServerClient.parseDeviceList(response); const removed = this.current.slice(); const added: AdbServerClient.Device[] = []; for (const nextDevice of next) { const index = this.current.findIndex( (device) => device.transportId === nextDevice.transportId, const index = removed.findIndex( (device) => device.transportId === nextDevice.transportId, ); if (index === -1) { added.push(nextDevice); continue; } unorderedRemove(this.current, index); unorderedRemove(removed, index); } this.current = next; if (added.length) { for (const observer of this.#observers) { observer.onDeviceAdd.fire(added); } } if (this.current.length) { if (removed.length) { for (const observer of this.#observers) { observer.onDeviceRemove.fire(this.current); observer.onDeviceRemove.fire(removed); } } this.current = next; for (const observer of this.#observers) { observer.onListChange.fire(this.current); } } async #receiveLoop(stream: AdbServerStream) { try { while (true) { await this.#receive(stream); } } catch (e) { this.#stream = undefined; for (const observer of this.#observers) { observer.onError.fire(e as Error); } Loading @@ -76,7 +87,11 @@ export class AdbServerDeviceObserverOwner { { unref: true }, ); void this.#receive(stream); // Set `current` and `onListChange` value before returning await this.#receive(stream); // Then start receive loop void this.#receiveLoop(stream); return stream; } Loading @@ -91,55 +106,65 @@ export class AdbServerDeviceObserverOwner { async createObserver( options?: AdbServerClient.ServerConnectionOptions, ): Promise<AdbServerClient.DeviceObserver> { if (options?.signal?.aborted) { throw options.signal.reason; } const onDeviceAdd = new EventEmitter<AdbServerClient.Device[]>(); const onDeviceRemove = new EventEmitter<AdbServerClient.Device[]>(); const onListChange = new StickyEventEmitter<AdbServerClient.Device[]>(); options?.signal?.throwIfAborted(); const onDeviceAdd = new EventEmitter< readonly AdbServerClient.Device[] >(); const onDeviceRemove = new EventEmitter< readonly AdbServerClient.Device[] >(); const onListChange = new StickyEventEmitter< readonly AdbServerClient.Device[] >(); const onError = new StickyEventEmitter<Error>(); const observer = { onDeviceAdd, onDeviceRemove, onListChange, onError }; // Register `observer` before `#connect`. // Because `#connect` might immediately receive some data // and want to trigger observers // So `#handleObserverStop` knows if there is any observer. this.#observers.push(observer); this.#stream ??= this.#connect(); const stream = await this.#stream; let stream: AdbServerStream; if (!this.#stream) { this.#stream = this.#connect(); if (options?.signal?.aborted) { await this.#handleObserverStop(stream); throw options.signal.reason; try { stream = await this.#stream; } catch (e) { this.#stream = undefined; throw e; } } else { stream = await this.#stream; onListChange.fire(this.current); } const ref = new Ref(options); const stop = async () => { const index = self.#observers.indexOf(observer); if (index === -1) { return; } unorderedRemove(this.#observers, index); unorderedRemove(this.#observers, this.#observers.indexOf(observer)); await this.#handleObserverStop(stream); ref.unref(); }; options?.signal?.addEventListener("abort", () => void stop()); if (options?.signal) { if (options.signal.aborted) { await stop(); throw options.signal.reason; } options.signal.addEventListener("abort", () => void stop()); } // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const _this = this; return { onDeviceAdd: onDeviceAdd.event, onDeviceRemove: onDeviceRemove.event, onListChange: onListChange.event, onError: onError.event, get current() { return self.current; return _this.current; }, stop, }; Loading libraries/event/src/event-emitter.ts +2 −13 Original line number Diff line number Diff line import type { Disposable } from "./disposable.js"; import type { EventListener, RemoveEventListener } from "./event.js"; import type { Event, EventListener, RemoveEventListener } from "./event.js"; export interface EventListenerInfo<TEvent, TResult = unknown> { listener: EventListener<TEvent, unknown, unknown[], TResult>; Loading @@ -9,17 +9,6 @@ export interface EventListenerInfo<TEvent, TResult = unknown> { args: unknown[]; } export interface AddEventListener<TEvent, TResult = unknown> { ( listener: EventListener<TEvent, unknown, [], TResult>, ): RemoveEventListener; <TThis, TArgs extends unknown[]>( listener: EventListener<TEvent, TThis, TArgs, TResult>, thisArg: TThis, ...args: TArgs ): RemoveEventListener; } export class EventEmitter<TEvent, TResult = unknown> implements Disposable { protected readonly listeners: EventListenerInfo<TEvent, TResult>[] = []; Loading @@ -42,7 +31,7 @@ export class EventEmitter<TEvent, TResult = unknown> implements Disposable { return remove; } event: AddEventListener<TEvent, TResult> = <TThis, TArgs extends unknown[]>( event: Event<TEvent, TResult> = <TThis, TArgs extends unknown[]>( listener: EventListener<TEvent, TThis, TArgs, TResult>, thisArg?: TThis, ...args: TArgs Loading Loading
.changeset/five-glasses-attend.md 0 → 100644 +7 −0 Original line number Diff line number Diff line --- "@yume-chan/adb-daemon-webusb": minor "@yume-chan/event": minor "@yume-chan/adb": minor --- Make `DeviceObserver#onListChange` sticky
libraries/adb-daemon-webusb/src/observer.ts +26 −10 Original line number Diff line number Diff line import type { DeviceObserver } from "@yume-chan/adb"; import { unorderedRemove } from "@yume-chan/adb"; import { EventEmitter, StickyEventEmitter } from "@yume-chan/event"; import { Loading @@ -24,20 +25,26 @@ export class AdbDaemonWebUsbDeviceObserver return new AdbDaemonWebUsbDeviceObserver(usb, devices, options); } #filters: (USBDeviceFilter & UsbInterfaceFilter)[]; #exclusionFilters?: USBDeviceFilter[] | undefined; #usbManager: USB; readonly #filters: (USBDeviceFilter & UsbInterfaceFilter)[]; readonly #exclusionFilters?: USBDeviceFilter[] | undefined; readonly #usbManager: USB; #onDeviceAdd = new EventEmitter<AdbDaemonWebUsbDevice[]>(); readonly #onDeviceAdd = new EventEmitter< readonly AdbDaemonWebUsbDevice[] >(); onDeviceAdd = this.#onDeviceAdd.event; #onDeviceRemove = new EventEmitter<AdbDaemonWebUsbDevice[]>(); readonly #onDeviceRemove = new EventEmitter< readonly AdbDaemonWebUsbDevice[] >(); onDeviceRemove = this.#onDeviceRemove.event; #onListChange = new StickyEventEmitter<AdbDaemonWebUsbDevice[]>(); readonly #onListChange = new StickyEventEmitter< readonly AdbDaemonWebUsbDevice[] >(); onListChange = this.#onListChange.event; current: AdbDaemonWebUsbDevice[] = []; current: readonly AdbDaemonWebUsbDevice[] = []; constructor( usb: USB, Loading @@ -47,9 +54,12 @@ export class AdbDaemonWebUsbDeviceObserver this.#filters = mergeDefaultAdbInterfaceFilter(options.filters); this.#exclusionFilters = options.exclusionFilters; this.#usbManager = usb; this.current = initial .map((device) => this.#convertDevice(device)) .filter((device) => !!device); // Fire `onListChange` to set the sticky value this.#onListChange.fire(this.current); this.#usbManager.addEventListener("connect", this.#handleConnect); this.#usbManager.addEventListener("disconnect", this.#handleDisconnect); Loading @@ -74,8 +84,11 @@ export class AdbDaemonWebUsbDeviceObserver return; } const next = this.current.slice(); next.push(device); this.current = next; this.#onDeviceAdd.fire([device]); this.current.push(device); this.#onListChange.fire(this.current); }; Loading @@ -85,9 +98,12 @@ export class AdbDaemonWebUsbDeviceObserver ); if (index !== -1) { const device = this.current[index]!; const next = this.current.slice(); unorderedRemove(next, index); this.current = next; this.#onDeviceRemove.fire([device]); this.current[index] = this.current[this.current.length - 1]!; this.current.length -= 1; this.#onListChange.fire(this.current); } }; Loading
libraries/adb/src/device-observer.ts +4 −4 Original line number Diff line number Diff line Loading @@ -2,9 +2,9 @@ import type { MaybePromiseLike } from "@yume-chan/async"; import type { Event } from "@yume-chan/event"; export interface DeviceObserver<T> { onDeviceAdd: Event<T[]>; onDeviceRemove: Event<T[]>; onListChange: Event<T[]>; current: T[]; readonly onDeviceAdd: Event<readonly T[]>; readonly onDeviceRemove: Event<readonly T[]>; readonly onListChange: Event<readonly T[]>; readonly current: readonly T[]; stop(): MaybePromiseLike<void>; }
libraries/adb/src/server/observer.ts +89 −64 Original line number Diff line number Diff line Loading @@ -5,20 +5,23 @@ import { Ref } from "../utils/index.js"; import { AdbServerClient } from "./client.js"; import type { AdbServerStream } from "./stream.js"; function unorderedRemove<T>(array: T[], index: number) { export function unorderedRemove<T>(array: T[], index: number) { if (index < 0 || index >= array.length) { return; } array[index] = array[array.length - 1]!; array.length -= 1; } export class AdbServerDeviceObserverOwner { current: AdbServerClient.Device[] = []; current: readonly AdbServerClient.Device[] = []; #client: AdbServerClient; readonly #client: AdbServerClient; #stream: Promise<AdbServerStream> | undefined; #observers: { onDeviceAdd: EventEmitter<AdbServerClient.Device[]>; onDeviceRemove: EventEmitter<AdbServerClient.Device[]>; onListChange: EventEmitter<AdbServerClient.Device[]>; onDeviceAdd: EventEmitter<readonly AdbServerClient.Device[]>; onDeviceRemove: EventEmitter<readonly AdbServerClient.Device[]>; onListChange: EventEmitter<readonly AdbServerClient.Device[]>; onError: EventEmitter<Error>; }[] = []; Loading @@ -27,42 +30,50 @@ export class AdbServerDeviceObserverOwner { } async #receive(stream: AdbServerStream) { try { while (true) { const response = await stream.readString(); const next = AdbServerClient.parseDeviceList(response); const removed = this.current.slice(); const added: AdbServerClient.Device[] = []; for (const nextDevice of next) { const index = this.current.findIndex( (device) => device.transportId === nextDevice.transportId, const index = removed.findIndex( (device) => device.transportId === nextDevice.transportId, ); if (index === -1) { added.push(nextDevice); continue; } unorderedRemove(this.current, index); unorderedRemove(removed, index); } this.current = next; if (added.length) { for (const observer of this.#observers) { observer.onDeviceAdd.fire(added); } } if (this.current.length) { if (removed.length) { for (const observer of this.#observers) { observer.onDeviceRemove.fire(this.current); observer.onDeviceRemove.fire(removed); } } this.current = next; for (const observer of this.#observers) { observer.onListChange.fire(this.current); } } async #receiveLoop(stream: AdbServerStream) { try { while (true) { await this.#receive(stream); } } catch (e) { this.#stream = undefined; for (const observer of this.#observers) { observer.onError.fire(e as Error); } Loading @@ -76,7 +87,11 @@ export class AdbServerDeviceObserverOwner { { unref: true }, ); void this.#receive(stream); // Set `current` and `onListChange` value before returning await this.#receive(stream); // Then start receive loop void this.#receiveLoop(stream); return stream; } Loading @@ -91,55 +106,65 @@ export class AdbServerDeviceObserverOwner { async createObserver( options?: AdbServerClient.ServerConnectionOptions, ): Promise<AdbServerClient.DeviceObserver> { if (options?.signal?.aborted) { throw options.signal.reason; } const onDeviceAdd = new EventEmitter<AdbServerClient.Device[]>(); const onDeviceRemove = new EventEmitter<AdbServerClient.Device[]>(); const onListChange = new StickyEventEmitter<AdbServerClient.Device[]>(); options?.signal?.throwIfAborted(); const onDeviceAdd = new EventEmitter< readonly AdbServerClient.Device[] >(); const onDeviceRemove = new EventEmitter< readonly AdbServerClient.Device[] >(); const onListChange = new StickyEventEmitter< readonly AdbServerClient.Device[] >(); const onError = new StickyEventEmitter<Error>(); const observer = { onDeviceAdd, onDeviceRemove, onListChange, onError }; // Register `observer` before `#connect`. // Because `#connect` might immediately receive some data // and want to trigger observers // So `#handleObserverStop` knows if there is any observer. this.#observers.push(observer); this.#stream ??= this.#connect(); const stream = await this.#stream; let stream: AdbServerStream; if (!this.#stream) { this.#stream = this.#connect(); if (options?.signal?.aborted) { await this.#handleObserverStop(stream); throw options.signal.reason; try { stream = await this.#stream; } catch (e) { this.#stream = undefined; throw e; } } else { stream = await this.#stream; onListChange.fire(this.current); } const ref = new Ref(options); const stop = async () => { const index = self.#observers.indexOf(observer); if (index === -1) { return; } unorderedRemove(this.#observers, index); unorderedRemove(this.#observers, this.#observers.indexOf(observer)); await this.#handleObserverStop(stream); ref.unref(); }; options?.signal?.addEventListener("abort", () => void stop()); if (options?.signal) { if (options.signal.aborted) { await stop(); throw options.signal.reason; } options.signal.addEventListener("abort", () => void stop()); } // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const _this = this; return { onDeviceAdd: onDeviceAdd.event, onDeviceRemove: onDeviceRemove.event, onListChange: onListChange.event, onError: onError.event, get current() { return self.current; return _this.current; }, stop, }; Loading
libraries/event/src/event-emitter.ts +2 −13 Original line number Diff line number Diff line import type { Disposable } from "./disposable.js"; import type { EventListener, RemoveEventListener } from "./event.js"; import type { Event, EventListener, RemoveEventListener } from "./event.js"; export interface EventListenerInfo<TEvent, TResult = unknown> { listener: EventListener<TEvent, unknown, unknown[], TResult>; Loading @@ -9,17 +9,6 @@ export interface EventListenerInfo<TEvent, TResult = unknown> { args: unknown[]; } export interface AddEventListener<TEvent, TResult = unknown> { ( listener: EventListener<TEvent, unknown, [], TResult>, ): RemoveEventListener; <TThis, TArgs extends unknown[]>( listener: EventListener<TEvent, TThis, TArgs, TResult>, thisArg: TThis, ...args: TArgs ): RemoveEventListener; } export class EventEmitter<TEvent, TResult = unknown> implements Disposable { protected readonly listeners: EventListenerInfo<TEvent, TResult>[] = []; Loading @@ -42,7 +31,7 @@ export class EventEmitter<TEvent, TResult = unknown> implements Disposable { return remove; } event: AddEventListener<TEvent, TResult> = <TThis, TArgs extends unknown[]>( event: Event<TEvent, TResult> = <TThis, TArgs extends unknown[]>( listener: EventListener<TEvent, TThis, TArgs, TResult>, thisArg?: TThis, ...args: TArgs Loading