@yume-chan/adb 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.json +39 -0
  2. package/CHANGELOG.md +20 -1
  3. package/README.md +46 -366
  4. package/esm/adb.d.ts +9 -5
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +9 -2
  7. package/esm/adb.js.map +1 -1
  8. package/esm/banner.d.ts.map +1 -1
  9. package/esm/banner.js +1 -0
  10. package/esm/banner.js.map +1 -1
  11. package/esm/commands/framebuffer.d.ts +1 -1
  12. package/esm/commands/framebuffer.d.ts.map +1 -1
  13. package/esm/commands/framebuffer.js.map +1 -1
  14. package/esm/commands/reverse.js +1 -1
  15. package/esm/commands/reverse.js.map +1 -1
  16. package/esm/commands/subprocess/command.js.map +1 -1
  17. package/esm/commands/subprocess/protocols/none.d.ts +2 -1
  18. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  19. package/esm/commands/subprocess/protocols/none.js +11 -14
  20. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  21. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  22. package/esm/commands/subprocess/protocols/shell.js +21 -71
  23. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  24. package/esm/commands/subprocess/utils.js +1 -1
  25. package/esm/commands/subprocess/utils.js.map +1 -1
  26. package/esm/commands/sync/list.d.ts +2 -1
  27. package/esm/commands/sync/list.d.ts.map +1 -1
  28. package/esm/commands/sync/list.js +2 -2
  29. package/esm/commands/sync/list.js.map +1 -1
  30. package/esm/commands/sync/pull.d.ts.map +1 -1
  31. package/esm/commands/sync/pull.js +4 -0
  32. package/esm/commands/sync/pull.js.map +1 -1
  33. package/esm/commands/sync/push.js.map +1 -1
  34. package/esm/commands/sync/request.js.map +1 -1
  35. package/esm/commands/sync/response.d.ts +1 -1
  36. package/esm/commands/sync/response.d.ts.map +1 -1
  37. package/esm/commands/sync/response.js +1 -1
  38. package/esm/commands/sync/response.js.map +1 -1
  39. package/esm/commands/sync/socket.d.ts +1 -0
  40. package/esm/commands/sync/socket.d.ts.map +1 -1
  41. package/esm/commands/sync/socket.js +4 -0
  42. package/esm/commands/sync/socket.js.map +1 -1
  43. package/esm/commands/sync/stat.js.map +1 -1
  44. package/esm/commands/sync/sync.d.ts.map +1 -1
  45. package/esm/commands/sync/sync.js +5 -6
  46. package/esm/commands/sync/sync.js.map +1 -1
  47. package/esm/commands/tcpip.d.ts +21 -0
  48. package/esm/commands/tcpip.d.ts.map +1 -1
  49. package/esm/commands/tcpip.js +18 -0
  50. package/esm/commands/tcpip.js.map +1 -1
  51. package/esm/daemon/auth.js.map +1 -1
  52. package/esm/daemon/crypto.js.map +1 -1
  53. package/esm/daemon/dispatcher.d.ts +9 -1
  54. package/esm/daemon/dispatcher.d.ts.map +1 -1
  55. package/esm/daemon/dispatcher.js +107 -51
  56. package/esm/daemon/dispatcher.js.map +1 -1
  57. package/esm/daemon/packet.d.ts +5 -5
  58. package/esm/daemon/packet.d.ts.map +1 -1
  59. package/esm/daemon/packet.js +1 -1
  60. package/esm/daemon/packet.js.map +1 -1
  61. package/esm/daemon/socket.d.ts +8 -13
  62. package/esm/daemon/socket.d.ts.map +1 -1
  63. package/esm/daemon/socket.js +80 -44
  64. package/esm/daemon/socket.js.map +1 -1
  65. package/esm/daemon/transport.d.ts +32 -4
  66. package/esm/daemon/transport.d.ts.map +1 -1
  67. package/esm/daemon/transport.js +61 -31
  68. package/esm/daemon/transport.js.map +1 -1
  69. package/esm/features.d.ts +2 -1
  70. package/esm/features.d.ts.map +1 -1
  71. package/esm/features.js +2 -1
  72. package/esm/features.js.map +1 -1
  73. package/esm/server/client.d.ts +14 -8
  74. package/esm/server/client.d.ts.map +1 -1
  75. package/esm/server/client.js +122 -70
  76. package/esm/server/client.js.map +1 -1
  77. package/esm/server/transport.d.ts +3 -0
  78. package/esm/server/transport.d.ts.map +1 -1
  79. package/esm/server/transport.js +26 -0
  80. package/esm/server/transport.js.map +1 -1
  81. package/esm/utils/auto-reset-event.js.map +1 -1
  82. package/esm/utils/base64.js.map +1 -1
  83. package/esm/utils/conditional-variable.js.map +1 -1
  84. package/esm/utils/hex.js.map +1 -1
  85. package/package.json +14 -14
  86. package/src/adb.ts +22 -7
  87. package/src/banner.ts +1 -0
  88. package/src/commands/framebuffer.ts +1 -1
  89. package/src/commands/reverse.ts +2 -2
  90. package/src/commands/subprocess/protocols/none.ts +15 -19
  91. package/src/commands/subprocess/protocols/shell.ts +28 -98
  92. package/src/commands/subprocess/utils.ts +1 -1
  93. package/src/commands/sync/list.ts +6 -2
  94. package/src/commands/sync/pull.ts +3 -0
  95. package/src/commands/sync/response.ts +2 -2
  96. package/src/commands/sync/socket.ts +5 -0
  97. package/src/commands/sync/sync.ts +5 -6
  98. package/src/commands/tcpip.ts +44 -0
  99. package/src/daemon/dispatcher.ts +156 -65
  100. package/src/daemon/packet.ts +1 -1
  101. package/src/daemon/socket.ts +104 -86
  102. package/src/daemon/transport.ts +104 -38
  103. package/src/features.ts +2 -1
  104. package/src/server/client.ts +141 -91
  105. package/src/server/transport.ts +28 -0
  106. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,4 +1,8 @@
1
- import { AsyncOperationManager, PromiseResolver } from "@yume-chan/async";
1
+ import {
2
+ AsyncOperationManager,
3
+ PromiseResolver,
4
+ delay,
5
+ } from "@yume-chan/async";
2
6
  import type {
3
7
  Consumable,
4
8
  ReadableWritablePair,
@@ -9,10 +13,10 @@ import {
9
13
  ConsumableWritableStream,
10
14
  WritableStream,
11
15
  } from "@yume-chan/stream-extra";
12
- import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
16
+ import { EMPTY_UINT8_ARRAY, NumberFieldType } from "@yume-chan/struct";
13
17
 
14
18
  import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
15
- import { decodeUtf8, encodeUtf8, unreachable } from "../utils/index.js";
19
+ import { decodeUtf8, encodeUtf8 } from "../utils/index.js";
16
20
 
17
21
  import type { AdbPacketData, AdbPacketInit } from "./packet.js";
18
22
  import { AdbCommand, calculateChecksum } from "./packet.js";
@@ -28,10 +32,23 @@ export interface AdbPacketDispatcherOptions {
28
32
  */
29
33
  appendNullToServiceString: boolean;
30
34
  maxPayloadSize: number;
35
+ /**
36
+ * The number of bytes the device can send before receiving an ack packet.
37
+
38
+ * Set to 0 or any negative value to disable delayed ack.
39
+ * Otherwise the value must be in the range of unsigned 32-bit integer.
40
+ */
41
+ initialDelayedAckBytes: number;
31
42
  /**
32
43
  * Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
33
44
  */
34
45
  preserveConnection?: boolean | undefined;
46
+ debugSlowRead?: boolean | undefined;
47
+ }
48
+
49
+ interface SocketOpenResult {
50
+ remoteId: number;
51
+ availableWriteBytes: number;
35
52
  }
36
53
 
37
54
  /**
@@ -74,36 +91,28 @@ export class AdbPacketDispatcher implements Closeable {
74
91
  options: AdbPacketDispatcherOptions,
75
92
  ) {
76
93
  this.options = options;
94
+ // Don't allow negative values in dispatcher
95
+ if (this.options.initialDelayedAckBytes < 0) {
96
+ this.options.initialDelayedAckBytes = 0;
97
+ }
77
98
 
78
99
  connection.readable
79
100
  .pipeTo(
80
101
  new WritableStream({
81
102
  write: async (packet) => {
82
103
  switch (packet.command) {
83
- case AdbCommand.OK:
84
- this.#handleOk(packet);
85
- break;
86
104
  case AdbCommand.Close:
87
105
  await this.#handleClose(packet);
88
106
  break;
89
- case AdbCommand.Write:
90
- if (this.#sockets.has(packet.arg1)) {
91
- await this.#sockets
92
- .get(packet.arg1)!
93
- .enqueue(packet.payload);
94
- await this.sendPacket(
95
- AdbCommand.OK,
96
- packet.arg1,
97
- packet.arg0,
98
- );
99
- break;
100
- }
101
- throw new Error(
102
- `Unknown local socket id: ${packet.arg1}`,
103
- );
107
+ case AdbCommand.Okay:
108
+ this.#handleOkay(packet);
109
+ break;
104
110
  case AdbCommand.Open:
105
111
  await this.#handleOpen(packet);
106
112
  break;
113
+ case AdbCommand.Write:
114
+ await this.#handleWrite(packet);
115
+ break;
107
116
  default:
108
117
  // Junk data may only appear in the authentication phase,
109
118
  // since the dispatcher only works after authentication,
@@ -137,24 +146,6 @@ export class AdbPacketDispatcher implements Closeable {
137
146
  this.#writer = connection.writable.getWriter();
138
147
  }
139
148
 
140
- #handleOk(packet: AdbPacketData) {
141
- if (this.#initializers.resolve(packet.arg1, packet.arg0)) {
142
- // Device successfully created the socket
143
- return;
144
- }
145
-
146
- const socket = this.#sockets.get(packet.arg1);
147
- if (socket) {
148
- // Device has received last `WRTE` to the socket
149
- socket.ack();
150
- return;
151
- }
152
-
153
- // Maybe the device is responding to a packet of last connection
154
- // Tell the device to close the socket
155
- void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
156
- }
157
-
158
149
  async #handleClose(packet: AdbPacketData) {
159
150
  // If the socket is still pending
160
151
  if (
@@ -182,15 +173,8 @@ export class AdbPacketDispatcher implements Closeable {
182
173
  // Ignore `arg0` and search for the socket
183
174
  const socket = this.#sockets.get(packet.arg1);
184
175
  if (socket) {
185
- // The device want to close the socket
186
- if (!socket.closed) {
187
- await this.sendPacket(
188
- AdbCommand.Close,
189
- packet.arg1,
190
- packet.arg0,
191
- );
192
- }
193
- await socket.dispose();
176
+ await socket.close();
177
+ socket.dispose();
194
178
  this.#sockets.delete(packet.arg1);
195
179
  return;
196
180
  }
@@ -200,16 +184,59 @@ export class AdbPacketDispatcher implements Closeable {
200
184
  // the device may also respond with two `CLSE` packets.
201
185
  }
202
186
 
203
- addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) {
204
- this.#incomingSocketHandlers.set(service, handler);
205
- }
187
+ #handleOkay(packet: AdbPacketData) {
188
+ let ackBytes: number;
189
+ if (this.options.initialDelayedAckBytes !== 0) {
190
+ if (packet.payload.byteLength !== 4) {
191
+ throw new Error(
192
+ "Invalid OKAY packet. Payload size should be 4",
193
+ );
194
+ }
195
+ ackBytes = NumberFieldType.Uint32.deserialize(packet.payload, true);
196
+ } else {
197
+ if (packet.payload.byteLength !== 0) {
198
+ throw new Error(
199
+ "Invalid OKAY packet. Payload size should be 0",
200
+ );
201
+ }
202
+ ackBytes = Infinity;
203
+ }
206
204
 
207
- removeReverseTunnel(address: string) {
208
- this.#incomingSocketHandlers.delete(address);
205
+ if (
206
+ this.#initializers.resolve(packet.arg1, {
207
+ remoteId: packet.arg0,
208
+ availableWriteBytes: ackBytes,
209
+ } satisfies SocketOpenResult)
210
+ ) {
211
+ // Device successfully created the socket
212
+ return;
213
+ }
214
+
215
+ const socket = this.#sockets.get(packet.arg1);
216
+ if (socket) {
217
+ // When delayed ack is enabled, `ackBytes` is a positive number represents
218
+ // how many bytes the device has received from this socket.
219
+ // When delayed ack is disabled, `ackBytes` is always `Infinity` represents
220
+ // the device has received last `WRTE` packet from the socket.
221
+ socket.ack(ackBytes);
222
+ return;
223
+ }
224
+
225
+ // Maybe the device is responding to a packet of last connection
226
+ // Tell the device to close the socket
227
+ void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
209
228
  }
210
229
 
211
- clearReverseTunnels() {
212
- this.#incomingSocketHandlers.clear();
230
+ #sendOkay(localId: number, remoteId: number, ackBytes: number) {
231
+ let payload: Uint8Array;
232
+ if (this.options.initialDelayedAckBytes !== 0) {
233
+ payload = new Uint8Array(4);
234
+ new DataView(payload.buffer).setUint32(0, ackBytes, true);
235
+ } else {
236
+ payload = EMPTY_UINT8_ARRAY;
237
+ }
238
+
239
+ return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
213
240
  }
214
241
 
215
242
  async #handleOpen(packet: AdbPacketData) {
@@ -219,9 +246,20 @@ export class AdbPacketDispatcher implements Closeable {
219
246
  this.#initializers.resolve(localId, undefined);
220
247
 
221
248
  const remoteId = packet.arg0;
222
- let service = decodeUtf8(packet.payload);
223
- if (service.endsWith("\0")) {
224
- service = service.substring(0, service.length - 1);
249
+ let initialDelayedAckBytes = packet.arg1;
250
+ const service = decodeUtf8(packet.payload);
251
+
252
+ if (this.options.initialDelayedAckBytes === 0) {
253
+ if (initialDelayedAckBytes !== 0) {
254
+ throw new Error("Invalid OPEN packet. arg1 should be 0");
255
+ }
256
+ initialDelayedAckBytes = Infinity;
257
+ } else {
258
+ if (initialDelayedAckBytes === 0) {
259
+ throw new Error(
260
+ "Invalid OPEN packet. arg1 should be greater than 0",
261
+ );
262
+ }
225
263
  }
226
264
 
227
265
  const handler = this.#incomingSocketHandlers.get(service);
@@ -237,26 +275,66 @@ export class AdbPacketDispatcher implements Closeable {
237
275
  localCreated: false,
238
276
  service,
239
277
  });
278
+ controller.ack(initialDelayedAckBytes);
240
279
 
241
280
  try {
242
281
  await handler(controller.socket);
243
282
  this.#sockets.set(localId, controller);
244
- await this.sendPacket(AdbCommand.OK, localId, remoteId);
283
+ await this.#sendOkay(
284
+ localId,
285
+ remoteId,
286
+ this.options.initialDelayedAckBytes,
287
+ );
245
288
  } catch (e) {
246
289
  await this.sendPacket(AdbCommand.Close, 0, remoteId);
247
290
  }
248
291
  }
249
292
 
293
+ async #handleWrite(packet: AdbPacketData) {
294
+ const socket = this.#sockets.get(packet.arg1);
295
+ if (!socket) {
296
+ throw new Error(`Unknown local socket id: ${packet.arg1}`);
297
+ }
298
+
299
+ let handled = false;
300
+ await Promise.race([
301
+ delay(5000).then(() => {
302
+ if (this.options.debugSlowRead && !handled) {
303
+ throw new Error(
304
+ `packet for \`${socket.service}\` not handled in 5 seconds`,
305
+ );
306
+ }
307
+ }),
308
+ (async () => {
309
+ await socket.enqueue(packet.payload);
310
+ await this.#sendOkay(
311
+ packet.arg1,
312
+ packet.arg0,
313
+ packet.payload.length,
314
+ );
315
+ handled = true;
316
+ })(),
317
+ ]);
318
+
319
+ return;
320
+ }
321
+
250
322
  async createSocket(service: string): Promise<AdbSocket> {
251
323
  if (this.options.appendNullToServiceString) {
252
324
  service += "\0";
253
325
  }
254
326
 
255
- const [localId, initializer] = this.#initializers.add<number>();
256
- await this.sendPacket(AdbCommand.Open, localId, 0, service);
327
+ const [localId, initializer] =
328
+ this.#initializers.add<SocketOpenResult>();
329
+ await this.sendPacket(
330
+ AdbCommand.Open,
331
+ localId,
332
+ this.options.initialDelayedAckBytes,
333
+ service,
334
+ );
257
335
 
258
- // Fulfilled by `handleOk`
259
- const remoteId = await initializer;
336
+ // Fulfilled by `handleOkay`
337
+ const { remoteId, availableWriteBytes } = await initializer;
260
338
  const controller = new AdbDaemonSocketController({
261
339
  dispatcher: this,
262
340
  localId,
@@ -264,11 +342,24 @@ export class AdbPacketDispatcher implements Closeable {
264
342
  localCreated: true,
265
343
  service,
266
344
  });
345
+ controller.ack(availableWriteBytes);
267
346
  this.#sockets.set(localId, controller);
268
347
 
269
348
  return controller.socket;
270
349
  }
271
350
 
351
+ addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) {
352
+ this.#incomingSocketHandlers.set(service, handler);
353
+ }
354
+
355
+ removeReverseTunnel(address: string) {
356
+ this.#incomingSocketHandlers.delete(address);
357
+ }
358
+
359
+ clearReverseTunnels() {
360
+ this.#incomingSocketHandlers.clear();
361
+ }
362
+
272
363
  async sendPacket(
273
364
  command: AdbCommand,
274
365
  arg0: number,
@@ -307,7 +398,7 @@ export class AdbPacketDispatcher implements Closeable {
307
398
  this.#closed = true;
308
399
 
309
400
  this.#readAbortController.abort();
310
- if (this.options.preserveConnection ?? false) {
401
+ if (this.options.preserveConnection) {
311
402
  this.#writer.releaseLock();
312
403
  } else {
313
404
  await this.#writer.close();
@@ -318,7 +409,7 @@ export class AdbPacketDispatcher implements Closeable {
318
409
 
319
410
  #dispose() {
320
411
  for (const socket of this.#sockets.values()) {
321
- socket.dispose().catch(unreachable);
412
+ socket.dispose();
322
413
  }
323
414
 
324
415
  this.#disconnected.resolve();
@@ -5,7 +5,7 @@ export enum AdbCommand {
5
5
  Auth = 0x48545541, // 'AUTH'
6
6
  Close = 0x45534c43, // 'CLSE'
7
7
  Connect = 0x4e584e43, // 'CNXN'
8
- OK = 0x59414b4f, // 'OKAY'
8
+ Okay = 0x59414b4f, // 'OKAY'
9
9
  Open = 0x4e45504f, // 'OPEN'
10
10
  Write = 0x45545257, // 'WRTE'
11
11
  }
@@ -1,21 +1,19 @@
1
1
  import { PromiseResolver } from "@yume-chan/async";
2
2
  import type { Disposable } from "@yume-chan/event";
3
3
  import type {
4
+ AbortSignal,
4
5
  Consumable,
5
6
  PushReadableStreamController,
6
7
  ReadableStream,
7
- ReadableWritablePair,
8
8
  WritableStream,
9
+ WritableStreamDefaultController,
9
10
  } from "@yume-chan/stream-extra";
10
11
  import {
11
12
  ConsumableWritableStream,
12
- DistributionStream,
13
- DuplexStreamFactory,
14
13
  PushReadableStream,
15
- pipeFrom,
16
14
  } from "@yume-chan/stream-extra";
17
15
 
18
- import type { Closeable } from "../adb.js";
16
+ import type { AdbSocket } from "../adb.js";
19
17
 
20
18
  import type { AdbPacketDispatcher } from "./dispatcher.js";
21
19
  import { AdbCommand } from "./packet.js";
@@ -36,11 +34,7 @@ export interface AdbDaemonSocketConstructionOptions
36
34
  }
37
35
 
38
36
  export class AdbDaemonSocketController
39
- implements
40
- AdbDaemonSocketInfo,
41
- ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>,
42
- Closeable,
43
- Disposable
37
+ implements AdbDaemonSocketInfo, AdbSocket, Disposable
44
38
  {
45
39
  readonly #dispatcher!: AdbPacketDispatcher;
46
40
 
@@ -49,25 +43,20 @@ export class AdbDaemonSocketController
49
43
  readonly localCreated!: boolean;
50
44
  readonly service!: string;
51
45
 
52
- #duplex: DuplexStreamFactory<Uint8Array, Consumable<Uint8Array>>;
53
-
54
46
  #readable: ReadableStream<Uint8Array>;
55
47
  #readableController!: PushReadableStreamController<Uint8Array>;
56
48
  get readable() {
57
49
  return this.#readable;
58
50
  }
59
51
 
60
- #writePromise: PromiseResolver<void> | undefined;
52
+ #writableController!: WritableStreamDefaultController;
61
53
  readonly writable: WritableStream<Consumable<Uint8Array>>;
62
54
 
63
55
  #closed = false;
64
- /**
65
- * Whether the socket is half-closed (i.e. the local side initiated the close).
66
- *
67
- * It's only used by dispatcher to avoid sending another `CLSE` packet to remote.
68
- */
56
+
57
+ #closedPromise = new PromiseResolver<void>();
69
58
  get closed() {
70
- return this.#closed;
59
+ return this.#closedPromise.promise;
71
60
  }
72
61
 
73
62
  #socket: AdbDaemonSocket;
@@ -75,6 +64,17 @@ export class AdbDaemonSocketController
75
64
  return this.#socket;
76
65
  }
77
66
 
67
+ #availableWriteBytesChanged: PromiseResolver<void> | undefined;
68
+ /**
69
+ * When delayed ack is disabled, can be `Infinity` if the socket is ready to write.
70
+ * Exactly one packet can be written no matter how large it is. Or `-1` if the socket
71
+ * is waiting for ack.
72
+ *
73
+ * When delayed ack is enabled, a non-negative finite number indicates the number of
74
+ * bytes that can be written to the socket before receiving an ack.
75
+ */
76
+ #availableWriteBytes = 0;
77
+
78
78
  constructor(options: AdbDaemonSocketConstructionOptions) {
79
79
  this.#dispatcher = options.dispatcher;
80
80
  this.localId = options.localId;
@@ -82,88 +82,110 @@ export class AdbDaemonSocketController
82
82
  this.localCreated = options.localCreated;
83
83
  this.service = options.service;
84
84
 
85
- // Check this image to help you understand the stream graph
86
- // cspell: disable-next-line
87
- // https://www.plantuml.com/plantuml/png/TL0zoeGm4ErpYc3l5JxyS0yWM6mX5j4C6p4cxcJ25ejttuGX88ZftizxUKmJI275pGhXl0PP_UkfK_CAz5Z2hcWsW9Ny2fdU4C1f5aSchFVxA8vJjlTPRhqZzDQMRB7AklwJ0xXtX0ZSKH1h24ghoKAdGY23FhxC4nS2pDvxzIvxb-8THU0XlEQJ-ZB7SnXTAvc_LhOckhMdLBnbtndpb-SB7a8q2SRD_W00
88
-
89
- this.#duplex = new DuplexStreamFactory<
90
- Uint8Array,
91
- Consumable<Uint8Array>
92
- >({
93
- close: async () => {
94
- this.#closed = true;
95
-
96
- await this.#dispatcher.sendPacket(
97
- AdbCommand.Close,
98
- this.localId,
99
- this.remoteId
100
- );
101
-
102
- // Don't `dispose` here, we need to wait for `CLSE` response packet.
103
- return false;
85
+ this.#readable = new PushReadableStream((controller) => {
86
+ this.#readableController = controller;
87
+ });
88
+
89
+ this.writable = new ConsumableWritableStream<Uint8Array>({
90
+ start: (controller) => {
91
+ this.#writableController = controller;
104
92
  },
105
- dispose: () => {
106
- // Error out the pending writes
107
- this.#writePromise?.reject(new Error("Socket closed"));
93
+ write: async (data, controller) => {
94
+ const size = data.length;
95
+ const chunkSize = this.#dispatcher.options.maxPayloadSize;
96
+ for (
97
+ let start = 0, end = chunkSize;
98
+ start < size;
99
+ start = end, end += chunkSize
100
+ ) {
101
+ const chunk = data.subarray(start, end);
102
+ await this.#writeChunk(chunk, controller.signal);
103
+ }
108
104
  },
109
105
  });
110
106
 
111
- this.#readable = this.#duplex.wrapReadable(
112
- new PushReadableStream(
113
- (controller) => {
114
- this.#readableController = controller;
115
- },
116
- {
117
- highWaterMark: options.highWaterMark ?? 16 * 1024,
118
- size(chunk) {
119
- return chunk.byteLength;
120
- },
121
- }
122
- )
123
- );
107
+ this.#socket = new AdbDaemonSocket(this);
108
+ }
124
109
 
125
- this.writable = pipeFrom(
126
- this.#duplex.createWritable(
127
- new ConsumableWritableStream<Uint8Array>({
128
- write: async (chunk) => {
129
- // Wait for an ack packet
130
- this.#writePromise = new PromiseResolver();
131
- await this.#dispatcher.sendPacket(
132
- AdbCommand.Write,
133
- this.localId,
134
- this.remoteId,
135
- chunk
136
- );
137
- await this.#writePromise.promise;
138
- },
139
- })
140
- ),
141
- new DistributionStream(this.#dispatcher.options.maxPayloadSize)
142
- );
110
+ async #writeChunk(data: Uint8Array, signal: AbortSignal) {
111
+ const length = data.byteLength;
112
+ while (this.#availableWriteBytes < length) {
113
+ // Only one lock is required because Web Streams API guarantees
114
+ // that `write` is not reentrant.
115
+ const resolver = new PromiseResolver<void>();
116
+ signal.addEventListener("abort", () => {
117
+ resolver.reject(signal.reason);
118
+ });
143
119
 
144
- this.#socket = new AdbDaemonSocket(this);
120
+ this.#availableWriteBytesChanged = resolver;
121
+ await resolver.promise;
122
+ }
123
+
124
+ if (this.#availableWriteBytes === Infinity) {
125
+ this.#availableWriteBytes = -1;
126
+ } else {
127
+ this.#availableWriteBytes -= length;
128
+ }
129
+
130
+ await this.#dispatcher.sendPacket(
131
+ AdbCommand.Write,
132
+ this.localId,
133
+ this.remoteId,
134
+ data,
135
+ );
145
136
  }
146
137
 
147
138
  async enqueue(data: Uint8Array) {
148
- // Consumer may abort the `ReadableStream` to close the socket,
149
- // it's OK to throw away further packets in this case.
139
+ // Consumers can `cancel` the `readable` if they are not interested in future data.
140
+ // Throw away the data if that happens.
150
141
  if (this.#readableController.abortSignal.aborted) {
151
142
  return;
152
143
  }
153
144
 
154
- await this.#readableController.enqueue(data);
145
+ try {
146
+ await this.#readableController.enqueue(data);
147
+ } catch (e) {
148
+ if (this.#readableController.abortSignal.aborted) {
149
+ return;
150
+ }
151
+ throw e;
152
+ }
155
153
  }
156
154
 
157
- ack() {
158
- this.#writePromise?.resolve();
155
+ public ack(bytes: number) {
156
+ this.#availableWriteBytes += bytes;
157
+ this.#availableWriteBytesChanged?.resolve();
159
158
  }
160
159
 
161
160
  async close(): Promise<void> {
162
- await this.#duplex.close();
161
+ if (this.#closed) {
162
+ return;
163
+ }
164
+ this.#closed = true;
165
+
166
+ this.#availableWriteBytesChanged?.reject(new Error("Socket closed"));
167
+
168
+ try {
169
+ this.#writableController.error(new Error("Socket closed"));
170
+ } catch {
171
+ // ignore
172
+ }
173
+
174
+ await this.#dispatcher.sendPacket(
175
+ AdbCommand.Close,
176
+ this.localId,
177
+ this.remoteId,
178
+ );
163
179
  }
164
180
 
165
181
  dispose() {
166
- return this.#duplex.dispose();
182
+ try {
183
+ this.#readableController.close();
184
+ } catch {
185
+ // ignore
186
+ }
187
+
188
+ this.#closedPromise.resolve();
167
189
  }
168
190
  }
169
191
 
@@ -175,11 +197,7 @@ export class AdbDaemonSocketController
175
197
  * `socket.writable.abort()`, `socket.writable.getWriter().abort()`,
176
198
  * `socket.writable.close()` or `socket.writable.getWriter().close()`.
177
199
  */
178
- export class AdbDaemonSocket
179
- implements
180
- AdbDaemonSocketInfo,
181
- ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
182
- {
200
+ export class AdbDaemonSocket implements AdbDaemonSocketInfo, AdbSocket {
183
201
  #controller: AdbDaemonSocketController;
184
202
 
185
203
  get localId(): number {
@@ -202,7 +220,7 @@ export class AdbDaemonSocket
202
220
  return this.#controller.writable;
203
221
  }
204
222
 
205
- get closed(): boolean {
223
+ get closed(): Promise<void> {
206
224
  return this.#controller.closed;
207
225
  }
208
226