Loading libraries/adb/src/daemon/dispatcher.ts +5 −4 Original line number Diff line number Diff line Loading @@ -214,9 +214,10 @@ export class AdbPacketDispatcher implements Closeable { const socket = this.#sockets.get(packet.arg1); if (socket) { // When delayed ack is enabled, device has received `ackBytes` from the socket. // When delayed ack is disabled, device has received last `WRTE` packet from the socket, // `ackBytes` is `Infinity` in this case. // When delayed ack is enabled, `ackBytes` is a positive number represents // how many bytes the device has received from this socket. // When delayed ack is disabled, `ackBytes` is always `Infinity` represents // the device has received last `WRTE` packet from the socket. socket.ack(ackBytes); return; } Loading Loading @@ -332,7 +333,7 @@ export class AdbPacketDispatcher implements Closeable { service, ); // Fulfilled by `handleOk` // Fulfilled by `handleOkay` const { remoteId, availableWriteBytes } = await initializer; const controller = new AdbDaemonSocketController({ dispatcher: this, Loading libraries/adb/src/daemon/socket.ts +30 −25 Original line number Diff line number Diff line import { PromiseResolver } from "@yume-chan/async"; import type { Disposable } from "@yume-chan/event"; import type { AbortSignal, Consumable, PushReadableStreamController, ReadableStream, Loading @@ -13,7 +14,6 @@ import { } from "@yume-chan/stream-extra"; import type { AdbSocket } from "../adb.js"; import { raceSignal } from "../server/index.js"; import type { AdbPacketDispatcher } from "./dispatcher.js"; import { AdbCommand } from "./packet.js"; Loading Loading @@ -105,16 +105,26 @@ export class AdbDaemonSocketController start = end, end += chunkSize ) { const chunk = data.subarray(start, end); const length = chunk.byteLength; await this.#writeChunk(chunk, controller.signal); } }, }); this.#socket = new AdbDaemonSocket(this); } async #writeChunk(data: Uint8Array, signal: AbortSignal) { const length = data.byteLength; while (this.#availableWriteBytes < length) { // Only one lock is required because Web Streams API guarantees // that `write` is not reentrant. this.#availableWriteBytesChanged = new PromiseResolver(); await raceSignal( () => this.#availableWriteBytesChanged!.promise, controller.signal, ); const resolver = new PromiseResolver<void>(); signal.addEventListener("abort", () => { resolver.reject(signal.reason); }); this.#availableWriteBytesChanged = resolver; await resolver.promise; } if (this.#availableWriteBytes === Infinity) { Loading @@ -127,14 +137,9 @@ export class AdbDaemonSocketController AdbCommand.Write, this.localId, this.remoteId, chunk, data, ); } }, }); this.#socket = new AdbDaemonSocket(this); } async enqueue(data: Uint8Array) { // Consumers can `cancel` the `readable` if they are not interested in future data. Loading libraries/stream-extra/src/wrap-writable.spec.ts 0 → 100644 +109 −0 Original line number Diff line number Diff line import { describe, expect, it, jest } from "@jest/globals"; import { WritableStream } from "./stream.js"; import { WrapWritableStream } from "./wrap-writable.js"; describe("WrapWritableStream", () => { describe("start", () => { it("should accept a WritableStream", async () => { const stream = new WritableStream(); const wrapper = new WrapWritableStream(stream); await wrapper.close(); expect(wrapper.writable).toBe(stream); }); it("should accept a start function", async () => { const stream = new WritableStream(); const start = jest.fn<() => WritableStream<number>>(() => stream); const wrapper = new WrapWritableStream(start); await stream.close(); expect(start).toHaveBeenCalledTimes(1); expect(wrapper.writable).toBe(stream); }); it("should accept a start object", async () => { const stream = new WritableStream(); const start = jest.fn<() => WritableStream<number>>(() => stream); const wrapper = new WrapWritableStream({ start }); await wrapper.close(); expect(start).toHaveBeenCalledTimes(1); expect(wrapper.writable).toBe(stream); }); }); describe("write", () => { it("should write to inner stream", async () => { const write = jest.fn<() => void>(); const stream = new WrapWritableStream( new WritableStream({ write, }), ); const writer = stream.getWriter(); const data = {}; await writer.write(data); await writer.close(); expect(write).toHaveBeenCalledTimes(1); expect(write).toHaveBeenCalledWith(data, expect.anything()); }); }); describe("close", () => { it("should close wrapper", async () => { const close = jest.fn<() => void>(); const stream = new WrapWritableStream({ start() { return new WritableStream(); }, close, }); await expect(stream.close()).resolves.toBe(undefined); expect(close).toHaveBeenCalledTimes(1); }); it("should close inner stream", async () => { const close = jest.fn<() => void>(); const stream = new WrapWritableStream( new WritableStream({ close, }), ); await expect(stream.close()).resolves.toBe(undefined); expect(close).toHaveBeenCalledTimes(1); }); it("should not close inner stream twice", async () => { const stream = new WrapWritableStream(new WritableStream()); await expect(stream.close()).resolves.toBe(undefined); }); }); describe("abort", () => { it("should close wrapper", async () => { const close = jest.fn<() => void>(); const stream = new WrapWritableStream({ start() { return new WritableStream(); }, close, }); await expect(stream.abort()).resolves.toBe(undefined); expect(close).toHaveBeenCalledTimes(1); }); it("should abort inner stream", async () => { const abort = jest.fn<() => void>(); const stream = new WrapWritableStream( new WritableStream({ abort, }), ); await expect(stream.abort()).resolves.toBe(undefined); expect(abort).toHaveBeenCalledTimes(1); }); it("should not close inner stream twice", async () => { const stream = new WrapWritableStream(new WritableStream()); await expect(stream.abort()).resolves.toBe(undefined); }); }); }); libraries/stream-extra/src/wrap-writable.ts +12 −12 Original line number Diff line number Diff line Loading @@ -13,19 +13,19 @@ export interface WritableStreamWrapper<T> { } async function getWrappedWritableStream<T>( wrapper: start: | WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>, ) { if ("start" in wrapper) { return await wrapper.start(); } else if (typeof wrapper === "function") { return await wrapper(); if ("start" in start) { return await start.start(); } else if (typeof start === "function") { return await start(); } else { // Can't use `wrapper instanceof WritableStream` // Because we want to be compatible with any WritableStream-like objects return wrapper; return start; } } Loading @@ -35,7 +35,7 @@ export class WrapWritableStream<T> extends WritableStream<T> { #writer!: WritableStreamDefaultWriter<T>; constructor( wrapper: start: | WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>, Loading @@ -48,7 +48,7 @@ export class WrapWritableStream<T> extends WritableStream<T> { // Queue a microtask to avoid this. await Promise.resolve(); this.writable = await getWrappedWritableStream(wrapper); this.writable = await getWrappedWritableStream(start); this.#writer = this.writable.getWriter(); }, write: async (chunk) => { Loading @@ -56,8 +56,8 @@ export class WrapWritableStream<T> extends WritableStream<T> { }, abort: async (reason) => { await this.#writer.abort(reason); if (wrapper !== this.writable && "close" in wrapper) { await wrapper.close?.(); if (start !== this.writable && "close" in start) { await start.close?.(); } }, close: async () => { Loading @@ -66,8 +66,8 @@ export class WrapWritableStream<T> extends WritableStream<T> { // closing the outer stream first will make the inner stream incapable of // sending data in its `close` handler. await this.#writer.close(); if (wrapper !== this.writable && "close" in wrapper) { await wrapper.close?.(); if (start !== this.writable && "close" in start) { await start.close?.(); } }, }); Loading Loading
libraries/adb/src/daemon/dispatcher.ts +5 −4 Original line number Diff line number Diff line Loading @@ -214,9 +214,10 @@ export class AdbPacketDispatcher implements Closeable { const socket = this.#sockets.get(packet.arg1); if (socket) { // When delayed ack is enabled, device has received `ackBytes` from the socket. // When delayed ack is disabled, device has received last `WRTE` packet from the socket, // `ackBytes` is `Infinity` in this case. // When delayed ack is enabled, `ackBytes` is a positive number represents // how many bytes the device has received from this socket. // When delayed ack is disabled, `ackBytes` is always `Infinity` represents // the device has received last `WRTE` packet from the socket. socket.ack(ackBytes); return; } Loading Loading @@ -332,7 +333,7 @@ export class AdbPacketDispatcher implements Closeable { service, ); // Fulfilled by `handleOk` // Fulfilled by `handleOkay` const { remoteId, availableWriteBytes } = await initializer; const controller = new AdbDaemonSocketController({ dispatcher: this, Loading
libraries/adb/src/daemon/socket.ts +30 −25 Original line number Diff line number Diff line import { PromiseResolver } from "@yume-chan/async"; import type { Disposable } from "@yume-chan/event"; import type { AbortSignal, Consumable, PushReadableStreamController, ReadableStream, Loading @@ -13,7 +14,6 @@ import { } from "@yume-chan/stream-extra"; import type { AdbSocket } from "../adb.js"; import { raceSignal } from "../server/index.js"; import type { AdbPacketDispatcher } from "./dispatcher.js"; import { AdbCommand } from "./packet.js"; Loading Loading @@ -105,16 +105,26 @@ export class AdbDaemonSocketController start = end, end += chunkSize ) { const chunk = data.subarray(start, end); const length = chunk.byteLength; await this.#writeChunk(chunk, controller.signal); } }, }); this.#socket = new AdbDaemonSocket(this); } async #writeChunk(data: Uint8Array, signal: AbortSignal) { const length = data.byteLength; while (this.#availableWriteBytes < length) { // Only one lock is required because Web Streams API guarantees // that `write` is not reentrant. this.#availableWriteBytesChanged = new PromiseResolver(); await raceSignal( () => this.#availableWriteBytesChanged!.promise, controller.signal, ); const resolver = new PromiseResolver<void>(); signal.addEventListener("abort", () => { resolver.reject(signal.reason); }); this.#availableWriteBytesChanged = resolver; await resolver.promise; } if (this.#availableWriteBytes === Infinity) { Loading @@ -127,14 +137,9 @@ export class AdbDaemonSocketController AdbCommand.Write, this.localId, this.remoteId, chunk, data, ); } }, }); this.#socket = new AdbDaemonSocket(this); } async enqueue(data: Uint8Array) { // Consumers can `cancel` the `readable` if they are not interested in future data. Loading
libraries/stream-extra/src/wrap-writable.spec.ts 0 → 100644 +109 −0 Original line number Diff line number Diff line import { describe, expect, it, jest } from "@jest/globals"; import { WritableStream } from "./stream.js"; import { WrapWritableStream } from "./wrap-writable.js"; describe("WrapWritableStream", () => { describe("start", () => { it("should accept a WritableStream", async () => { const stream = new WritableStream(); const wrapper = new WrapWritableStream(stream); await wrapper.close(); expect(wrapper.writable).toBe(stream); }); it("should accept a start function", async () => { const stream = new WritableStream(); const start = jest.fn<() => WritableStream<number>>(() => stream); const wrapper = new WrapWritableStream(start); await stream.close(); expect(start).toHaveBeenCalledTimes(1); expect(wrapper.writable).toBe(stream); }); it("should accept a start object", async () => { const stream = new WritableStream(); const start = jest.fn<() => WritableStream<number>>(() => stream); const wrapper = new WrapWritableStream({ start }); await wrapper.close(); expect(start).toHaveBeenCalledTimes(1); expect(wrapper.writable).toBe(stream); }); }); describe("write", () => { it("should write to inner stream", async () => { const write = jest.fn<() => void>(); const stream = new WrapWritableStream( new WritableStream({ write, }), ); const writer = stream.getWriter(); const data = {}; await writer.write(data); await writer.close(); expect(write).toHaveBeenCalledTimes(1); expect(write).toHaveBeenCalledWith(data, expect.anything()); }); }); describe("close", () => { it("should close wrapper", async () => { const close = jest.fn<() => void>(); const stream = new WrapWritableStream({ start() { return new WritableStream(); }, close, }); await expect(stream.close()).resolves.toBe(undefined); expect(close).toHaveBeenCalledTimes(1); }); it("should close inner stream", async () => { const close = jest.fn<() => void>(); const stream = new WrapWritableStream( new WritableStream({ close, }), ); await expect(stream.close()).resolves.toBe(undefined); expect(close).toHaveBeenCalledTimes(1); }); it("should not close inner stream twice", async () => { const stream = new WrapWritableStream(new WritableStream()); await expect(stream.close()).resolves.toBe(undefined); }); }); describe("abort", () => { it("should close wrapper", async () => { const close = jest.fn<() => void>(); const stream = new WrapWritableStream({ start() { return new WritableStream(); }, close, }); await expect(stream.abort()).resolves.toBe(undefined); expect(close).toHaveBeenCalledTimes(1); }); it("should abort inner stream", async () => { const abort = jest.fn<() => void>(); const stream = new WrapWritableStream( new WritableStream({ abort, }), ); await expect(stream.abort()).resolves.toBe(undefined); expect(abort).toHaveBeenCalledTimes(1); }); it("should not close inner stream twice", async () => { const stream = new WrapWritableStream(new WritableStream()); await expect(stream.abort()).resolves.toBe(undefined); }); }); });
libraries/stream-extra/src/wrap-writable.ts +12 −12 Original line number Diff line number Diff line Loading @@ -13,19 +13,19 @@ export interface WritableStreamWrapper<T> { } async function getWrappedWritableStream<T>( wrapper: start: | WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>, ) { if ("start" in wrapper) { return await wrapper.start(); } else if (typeof wrapper === "function") { return await wrapper(); if ("start" in start) { return await start.start(); } else if (typeof start === "function") { return await start(); } else { // Can't use `wrapper instanceof WritableStream` // Because we want to be compatible with any WritableStream-like objects return wrapper; return start; } } Loading @@ -35,7 +35,7 @@ export class WrapWritableStream<T> extends WritableStream<T> { #writer!: WritableStreamDefaultWriter<T>; constructor( wrapper: start: | WritableStream<T> | WrapWritableStreamStart<T> | WritableStreamWrapper<T>, Loading @@ -48,7 +48,7 @@ export class WrapWritableStream<T> extends WritableStream<T> { // Queue a microtask to avoid this. await Promise.resolve(); this.writable = await getWrappedWritableStream(wrapper); this.writable = await getWrappedWritableStream(start); this.#writer = this.writable.getWriter(); }, write: async (chunk) => { Loading @@ -56,8 +56,8 @@ export class WrapWritableStream<T> extends WritableStream<T> { }, abort: async (reason) => { await this.#writer.abort(reason); if (wrapper !== this.writable && "close" in wrapper) { await wrapper.close?.(); if (start !== this.writable && "close" in start) { await start.close?.(); } }, close: async () => { Loading @@ -66,8 +66,8 @@ export class WrapWritableStream<T> extends WritableStream<T> { // closing the outer stream first will make the inner stream incapable of // sending data in its `close` handler. await this.#writer.close(); if (wrapper !== this.writable && "close" in wrapper) { await wrapper.close?.(); if (start !== this.writable && "close" in start) { await start.close?.(); } }, }); Loading