@yume-chan/adb 0.0.22 → 0.0.24

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 (149) hide show
  1. package/CHANGELOG.json +51 -0
  2. package/CHANGELOG.md +24 -1
  3. package/LICENSE +1 -1
  4. package/esm/adb.d.ts +6 -3
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +11 -4
  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/power.d.ts.map +1 -1
  12. package/esm/commands/power.js +6 -5
  13. package/esm/commands/power.js.map +1 -1
  14. package/esm/commands/reverse.d.ts.map +1 -1
  15. package/esm/commands/reverse.js +23 -11
  16. package/esm/commands/reverse.js.map +1 -1
  17. package/esm/commands/subprocess/command.js +3 -3
  18. package/esm/commands/subprocess/command.js.map +1 -1
  19. package/esm/commands/subprocess/protocols/none.d.ts +2 -2
  20. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  21. package/esm/commands/subprocess/protocols/shell.d.ts +3 -3
  22. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  23. package/esm/commands/subprocess/protocols/shell.js +14 -69
  24. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  25. package/esm/commands/subprocess/protocols/types.d.ts +2 -2
  26. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  27. package/esm/commands/subprocess/utils.js +1 -1
  28. package/esm/commands/subprocess/utils.js.map +1 -1
  29. package/esm/commands/sync/list.d.ts +2 -5
  30. package/esm/commands/sync/list.d.ts.map +1 -1
  31. package/esm/commands/sync/list.js +2 -4
  32. package/esm/commands/sync/list.js.map +1 -1
  33. package/esm/commands/sync/pull.d.ts +1 -4
  34. package/esm/commands/sync/pull.d.ts.map +1 -1
  35. package/esm/commands/sync/pull.js +2 -2
  36. package/esm/commands/sync/pull.js.map +1 -1
  37. package/esm/commands/sync/push.d.ts +4 -7
  38. package/esm/commands/sync/push.d.ts.map +1 -1
  39. package/esm/commands/sync/push.js +3 -6
  40. package/esm/commands/sync/push.js.map +1 -1
  41. package/esm/commands/sync/request.d.ts +13 -18
  42. package/esm/commands/sync/request.d.ts.map +1 -1
  43. package/esm/commands/sync/request.js +23 -28
  44. package/esm/commands/sync/request.js.map +1 -1
  45. package/esm/commands/sync/response.d.ts +18 -12
  46. package/esm/commands/sync/response.d.ts.map +1 -1
  47. package/esm/commands/sync/response.js +39 -17
  48. package/esm/commands/sync/response.js.map +1 -1
  49. package/esm/commands/sync/socket.d.ts +2 -2
  50. package/esm/commands/sync/socket.d.ts.map +1 -1
  51. package/esm/commands/sync/socket.js +10 -5
  52. package/esm/commands/sync/socket.js.map +1 -1
  53. package/esm/commands/sync/stat.d.ts +0 -3
  54. package/esm/commands/sync/stat.d.ts.map +1 -1
  55. package/esm/commands/sync/stat.js +0 -2
  56. package/esm/commands/sync/stat.js.map +1 -1
  57. package/esm/commands/sync/sync.d.ts +4 -3
  58. package/esm/commands/sync/sync.d.ts.map +1 -1
  59. package/esm/commands/sync/sync.js +9 -7
  60. package/esm/commands/sync/sync.js.map +1 -1
  61. package/esm/commands/tcpip.js +1 -1
  62. package/esm/commands/tcpip.js.map +1 -1
  63. package/esm/daemon/auth.js +1 -1
  64. package/esm/daemon/auth.js.map +1 -1
  65. package/esm/daemon/crypto.d.ts +3 -2
  66. package/esm/daemon/crypto.d.ts.map +1 -1
  67. package/esm/daemon/crypto.js +56 -49
  68. package/esm/daemon/crypto.js.map +1 -1
  69. package/esm/daemon/dispatcher.d.ts +38 -6
  70. package/esm/daemon/dispatcher.d.ts.map +1 -1
  71. package/esm/daemon/dispatcher.js +86 -28
  72. package/esm/daemon/dispatcher.js.map +1 -1
  73. package/esm/daemon/packet.d.ts +2 -2
  74. package/esm/daemon/packet.d.ts.map +1 -1
  75. package/esm/daemon/packet.js +13 -12
  76. package/esm/daemon/packet.js.map +1 -1
  77. package/esm/daemon/socket.d.ts +11 -6
  78. package/esm/daemon/socket.d.ts.map +1 -1
  79. package/esm/daemon/socket.js +42 -12
  80. package/esm/daemon/socket.js.map +1 -1
  81. package/esm/daemon/transport.d.ts +75 -11
  82. package/esm/daemon/transport.d.ts.map +1 -1
  83. package/esm/daemon/transport.js +60 -33
  84. package/esm/daemon/transport.js.map +1 -1
  85. package/esm/features.d.ts +2 -1
  86. package/esm/features.d.ts.map +1 -1
  87. package/esm/features.js +2 -1
  88. package/esm/features.js.map +1 -1
  89. package/esm/server/client.d.ts +128 -48
  90. package/esm/server/client.d.ts.map +1 -1
  91. package/esm/server/client.js +378 -145
  92. package/esm/server/client.js.map +1 -1
  93. package/esm/server/transport.d.ts +3 -0
  94. package/esm/server/transport.d.ts.map +1 -1
  95. package/esm/server/transport.js +30 -4
  96. package/esm/server/transport.js.map +1 -1
  97. package/esm/utils/base64.d.ts.map +1 -1
  98. package/esm/utils/base64.js +33 -22
  99. package/esm/utils/base64.js.map +1 -1
  100. package/esm/utils/hex.d.ts +1 -1
  101. package/esm/utils/hex.d.ts.map +1 -1
  102. package/esm/utils/hex.js +12 -14
  103. package/esm/utils/hex.js.map +1 -1
  104. package/esm/utils/index.d.ts +1 -1
  105. package/esm/utils/index.d.ts.map +1 -1
  106. package/esm/utils/index.js +1 -1
  107. package/esm/utils/index.js.map +1 -1
  108. package/esm/utils/sequence-equal.d.ts +2 -0
  109. package/esm/utils/sequence-equal.d.ts.map +1 -0
  110. package/esm/utils/sequence-equal.js +12 -0
  111. package/esm/utils/sequence-equal.js.map +1 -0
  112. package/package.json +13 -15
  113. package/src/adb.ts +22 -6
  114. package/src/banner.ts +1 -0
  115. package/src/commands/power.ts +7 -5
  116. package/src/commands/reverse.ts +31 -18
  117. package/src/commands/subprocess/command.ts +3 -3
  118. package/src/commands/subprocess/protocols/none.ts +2 -2
  119. package/src/commands/subprocess/protocols/shell.ts +21 -100
  120. package/src/commands/subprocess/protocols/types.ts +2 -2
  121. package/src/commands/subprocess/utils.ts +1 -1
  122. package/src/commands/sync/list.ts +2 -4
  123. package/src/commands/sync/pull.ts +2 -2
  124. package/src/commands/sync/push.ts +7 -10
  125. package/src/commands/sync/request.ts +32 -32
  126. package/src/commands/sync/response.ts +47 -20
  127. package/src/commands/sync/socket.ts +13 -8
  128. package/src/commands/sync/stat.ts +0 -2
  129. package/src/commands/sync/sync.ts +13 -9
  130. package/src/commands/tcpip.ts +1 -1
  131. package/src/daemon/auth.ts +1 -1
  132. package/src/daemon/crypto.ts +64 -55
  133. package/src/daemon/dispatcher.ts +175 -37
  134. package/src/daemon/packet.ts +21 -14
  135. package/src/daemon/socket.ts +62 -28
  136. package/src/daemon/transport.ts +145 -40
  137. package/src/features.ts +2 -1
  138. package/src/server/client.ts +472 -224
  139. package/src/server/transport.ts +32 -4
  140. package/src/utils/base64.ts +33 -22
  141. package/src/utils/hex.ts +16 -14
  142. package/src/utils/index.ts +1 -1
  143. package/src/utils/sequence-equal.ts +13 -0
  144. package/tsconfig.build.tsbuildinfo +1 -1
  145. package/esm/utils/conditional-variable.d.ts +0 -8
  146. package/esm/utils/conditional-variable.d.ts.map +0 -1
  147. package/esm/utils/conditional-variable.js +0 -34
  148. package/esm/utils/conditional-variable.js.map +0 -1
  149. package/src/utils/conditional-variable.ts +0 -45
@@ -3,40 +3,83 @@ import {
3
3
  PromiseResolver,
4
4
  delay,
5
5
  } from "@yume-chan/async";
6
+ import {
7
+ getUint32LittleEndian,
8
+ setUint32LittleEndian,
9
+ } from "@yume-chan/no-data-view";
6
10
  import type {
7
- Consumable,
8
11
  ReadableWritablePair,
9
12
  WritableStreamDefaultWriter,
10
13
  } from "@yume-chan/stream-extra";
11
14
  import {
12
15
  AbortController,
13
- ConsumableWritableStream,
16
+ Consumable,
14
17
  WritableStream,
15
18
  } from "@yume-chan/stream-extra";
16
- import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
19
+ import { EMPTY_UINT8_ARRAY, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
17
20
 
18
21
  import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
19
- import { decodeUtf8, encodeUtf8 } from "../utils/index.js";
20
22
 
21
23
  import type { AdbPacketData, AdbPacketInit } from "./packet.js";
22
24
  import { AdbCommand, calculateChecksum } from "./packet.js";
23
25
  import { AdbDaemonSocketController } from "./socket.js";
24
26
 
25
27
  export interface AdbPacketDispatcherOptions {
28
+ /**
29
+ * From Android 9.0, ADB stopped checking the checksum in packet header to improve performance.
30
+ *
31
+ * The value should be inferred from the device's ADB protocol version.
32
+ */
26
33
  calculateChecksum: boolean;
34
+
27
35
  /**
28
- * Before Android 9.0, ADB uses `char*` to parse service string,
36
+ * Before Android 9.0, ADB uses `char*` to parse service strings,
29
37
  * thus requires a null character to terminate.
30
38
  *
31
- * Usually it should have the same value as `calculateChecksum`.
39
+ * The value should be inferred from the device's ADB protocol version.
40
+ * Usually it should have the same value as `calculateChecksum`, since they both changed
41
+ * in Android 9.0.
32
42
  */
33
43
  appendNullToServiceString: boolean;
44
+
34
45
  maxPayloadSize: number;
46
+
35
47
  /**
36
- * Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
48
+ * Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`)
49
+ * when `AdbPacketDispatcher.close` is called.
50
+ *
51
+ * @default false
37
52
  */
38
53
  preserveConnection?: boolean | undefined;
39
- debugSlowRead?: boolean | undefined;
54
+
55
+ /**
56
+ * The number of bytes the device can send before receiving an ack packet.
57
+ * Using delayed ack can improve the throughput,
58
+ * especially when the device is connected over Wi-Fi (so the latency is higher).
59
+ *
60
+ * This must be the negotiated value between the client and device. If the device enabled
61
+ * delayed ack but the client didn't, the device will throw an error when the client sends
62
+ * the first `WRTE` packet. And vice versa.
63
+ */
64
+ initialDelayedAckBytes: number;
65
+
66
+ /**
67
+ * When set, the dispatcher will throw an error when
68
+ * one of the socket readable stalls for this amount of milliseconds.
69
+ *
70
+ * Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets.
71
+ * It's important to always read from all sockets to prevent stalling.
72
+ *
73
+ * This option is helpful to detect bugs in the client code.
74
+ *
75
+ * @default false
76
+ */
77
+ readTimeLimit?: number | undefined;
78
+ }
79
+
80
+ interface SocketOpenResult {
81
+ remoteId: number;
82
+ availableWriteBytes: number;
40
83
  }
41
84
 
42
85
  /**
@@ -79,6 +122,10 @@ export class AdbPacketDispatcher implements Closeable {
79
122
  options: AdbPacketDispatcherOptions,
80
123
  ) {
81
124
  this.options = options;
125
+ // Don't allow negative values in dispatcher
126
+ if (this.options.initialDelayedAckBytes < 0) {
127
+ this.options.initialDelayedAckBytes = 0;
128
+ }
82
129
 
83
130
  connection.readable
84
131
  .pipeTo(
@@ -169,38 +216,104 @@ export class AdbPacketDispatcher implements Closeable {
169
216
  }
170
217
 
171
218
  #handleOkay(packet: AdbPacketData) {
172
- if (this.#initializers.resolve(packet.arg1, packet.arg0)) {
219
+ let ackBytes: number;
220
+ if (this.options.initialDelayedAckBytes !== 0) {
221
+ if (packet.payload.length !== 4) {
222
+ throw new Error(
223
+ "Invalid OKAY packet. Payload size should be 4",
224
+ );
225
+ }
226
+ ackBytes = getUint32LittleEndian(packet.payload, 0);
227
+ } else {
228
+ if (packet.payload.length !== 0) {
229
+ throw new Error(
230
+ "Invalid OKAY packet. Payload size should be 0",
231
+ );
232
+ }
233
+ ackBytes = Infinity;
234
+ }
235
+
236
+ if (
237
+ this.#initializers.resolve(packet.arg1, {
238
+ remoteId: packet.arg0,
239
+ availableWriteBytes: ackBytes,
240
+ } satisfies SocketOpenResult)
241
+ ) {
173
242
  // Device successfully created the socket
174
243
  return;
175
244
  }
176
245
 
177
246
  const socket = this.#sockets.get(packet.arg1);
178
247
  if (socket) {
179
- // Device has received last `WRTE` to the socket
180
- socket.ack();
248
+ // When delayed ack is enabled, `ackBytes` is a positive number represents
249
+ // how many bytes the device has received from this socket.
250
+ // When delayed ack is disabled, `ackBytes` is always `Infinity` represents
251
+ // the device has received last `WRTE` packet from the socket.
252
+ socket.ack(ackBytes);
181
253
  return;
182
254
  }
183
255
 
184
256
  // Maybe the device is responding to a packet of last connection
185
257
  // Tell the device to close the socket
186
- void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
258
+ void this.sendPacket(
259
+ AdbCommand.Close,
260
+ packet.arg1,
261
+ packet.arg0,
262
+ EMPTY_UINT8_ARRAY,
263
+ );
264
+ }
265
+
266
+ #sendOkay(localId: number, remoteId: number, ackBytes: number) {
267
+ let payload: Uint8Array;
268
+ if (this.options.initialDelayedAckBytes !== 0) {
269
+ // TODO: try reusing this buffer to reduce memory allocation
270
+ // However, that requires blocking reentrance of `sendOkay`, which might be more expensive
271
+ payload = new Uint8Array(4);
272
+ setUint32LittleEndian(payload, 0, ackBytes);
273
+ } else {
274
+ payload = EMPTY_UINT8_ARRAY;
275
+ }
276
+
277
+ return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
187
278
  }
188
279
 
189
280
  async #handleOpen(packet: AdbPacketData) {
190
- // `AsyncOperationManager` doesn't support skipping IDs
191
- // Use `add` + `resolve` to simulate this behavior
281
+ // Allocate a local ID for the socket from `#initializers`.
282
+ // `AsyncOperationManager` doesn't directly support returning the next ID,
283
+ // so use `add` + `resolve` to simulate this
192
284
  const [localId] = this.#initializers.add<number>();
193
285
  this.#initializers.resolve(localId, undefined);
194
286
 
195
287
  const remoteId = packet.arg0;
288
+ let availableWriteBytes = packet.arg1;
196
289
  let service = decodeUtf8(packet.payload);
290
+ // ADB Daemon still adds a null character to the service string
197
291
  if (service.endsWith("\0")) {
198
292
  service = service.substring(0, service.length - 1);
199
293
  }
200
294
 
295
+ // Check remote delayed ack enablement is consistent with local
296
+ if (this.options.initialDelayedAckBytes === 0) {
297
+ if (availableWriteBytes !== 0) {
298
+ throw new Error("Invalid OPEN packet. arg1 should be 0");
299
+ }
300
+ availableWriteBytes = Infinity;
301
+ } else {
302
+ if (availableWriteBytes === 0) {
303
+ throw new Error(
304
+ "Invalid OPEN packet. arg1 should be greater than 0",
305
+ );
306
+ }
307
+ }
308
+
201
309
  const handler = this.#incomingSocketHandlers.get(service);
202
310
  if (!handler) {
203
- await this.sendPacket(AdbCommand.Close, 0, remoteId);
311
+ await this.sendPacket(
312
+ AdbCommand.Close,
313
+ 0,
314
+ remoteId,
315
+ EMPTY_UINT8_ARRAY,
316
+ );
204
317
  return;
205
318
  }
206
319
 
@@ -210,14 +323,24 @@ export class AdbPacketDispatcher implements Closeable {
210
323
  remoteId,
211
324
  localCreated: false,
212
325
  service,
326
+ availableWriteBytes,
213
327
  });
214
328
 
215
329
  try {
216
330
  await handler(controller.socket);
217
331
  this.#sockets.set(localId, controller);
218
- await this.sendPacket(AdbCommand.Okay, localId, remoteId);
332
+ await this.#sendOkay(
333
+ localId,
334
+ remoteId,
335
+ this.options.initialDelayedAckBytes,
336
+ );
219
337
  } catch (e) {
220
- await this.sendPacket(AdbCommand.Close, 0, remoteId);
338
+ await this.sendPacket(
339
+ AdbCommand.Close,
340
+ 0,
341
+ remoteId,
342
+ EMPTY_UINT8_ARRAY,
343
+ );
221
344
  }
222
345
  }
223
346
 
@@ -228,26 +351,33 @@ export class AdbPacketDispatcher implements Closeable {
228
351
  }
229
352
 
230
353
  let handled = false;
231
- await Promise.race([
232
- delay(5000).then(() => {
233
- if (this.options.debugSlowRead && !handled) {
234
- throw new Error(
235
- `packet for \`${socket.service}\` not handled in 5 seconds`,
236
- );
237
- }
238
- }),
354
+
355
+ const promises: Promise<void>[] = [
239
356
  (async () => {
240
357
  await socket.enqueue(packet.payload);
241
- await this.sendPacket(
242
- AdbCommand.Okay,
358
+ await this.#sendOkay(
243
359
  packet.arg1,
244
360
  packet.arg0,
361
+ packet.payload.length,
245
362
  );
246
363
  handled = true;
247
364
  })(),
248
- ]);
365
+ ];
366
+
367
+ if (this.options.readTimeLimit) {
368
+ promises.push(
369
+ (async () => {
370
+ await delay(this.options.readTimeLimit!);
371
+ if (!handled) {
372
+ throw new Error(
373
+ `readable of \`${socket.service}\` has stalled for ${this.options.readTimeLimit} milliseconds`,
374
+ );
375
+ }
376
+ })(),
377
+ );
378
+ }
249
379
 
250
- return;
380
+ await Promise.race(promises);
251
381
  }
252
382
 
253
383
  async createSocket(service: string): Promise<AdbSocket> {
@@ -255,17 +385,24 @@ export class AdbPacketDispatcher implements Closeable {
255
385
  service += "\0";
256
386
  }
257
387
 
258
- const [localId, initializer] = this.#initializers.add<number>();
259
- await this.sendPacket(AdbCommand.Open, localId, 0, service);
388
+ const [localId, initializer] =
389
+ this.#initializers.add<SocketOpenResult>();
390
+ await this.sendPacket(
391
+ AdbCommand.Open,
392
+ localId,
393
+ this.options.initialDelayedAckBytes,
394
+ service,
395
+ );
260
396
 
261
- // Fulfilled by `handleOk`
262
- const remoteId = await initializer;
397
+ // Fulfilled by `handleOkay`
398
+ const { remoteId, availableWriteBytes } = await initializer;
263
399
  const controller = new AdbDaemonSocketController({
264
400
  dispatcher: this,
265
401
  localId,
266
402
  remoteId,
267
403
  localCreated: true,
268
404
  service,
405
+ availableWriteBytes,
269
406
  });
270
407
  this.#sockets.set(localId, controller);
271
408
 
@@ -288,17 +425,18 @@ export class AdbPacketDispatcher implements Closeable {
288
425
  command: AdbCommand,
289
426
  arg0: number,
290
427
  arg1: number,
291
- payload: string | Uint8Array = EMPTY_UINT8_ARRAY,
428
+ // PERF: It's slightly faster to not use default parameter values
429
+ payload: string | Uint8Array,
292
430
  ): Promise<void> {
293
431
  if (typeof payload === "string") {
294
432
  payload = encodeUtf8(payload);
295
433
  }
296
434
 
297
- if (payload.byteLength > this.options.maxPayloadSize) {
298
- throw new Error("payload too large");
435
+ if (payload.length > this.options.maxPayloadSize) {
436
+ throw new TypeError("payload too large");
299
437
  }
300
438
 
301
- await ConsumableWritableStream.write(this.#writer, {
439
+ await Consumable.WritableStream.write(this.#writer, {
302
440
  command,
303
441
  arg0,
304
442
  arg1,
@@ -1,4 +1,4 @@
1
- import { ConsumableTransformStream } from "@yume-chan/stream-extra";
1
+ import { Consumable, TransformStream } from "@yume-chan/stream-extra";
2
2
  import Struct from "@yume-chan/struct";
3
3
 
4
4
  export enum AdbCommand {
@@ -49,26 +49,33 @@ export function calculateChecksum(payload: Uint8Array): number {
49
49
  return payload.reduce((result, item) => result + item, 0);
50
50
  }
51
51
 
52
- export class AdbPacketSerializeStream extends ConsumableTransformStream<
53
- AdbPacketInit,
54
- Uint8Array
52
+ export class AdbPacketSerializeStream extends TransformStream<
53
+ Consumable<AdbPacketInit>,
54
+ Consumable<Uint8Array>
55
55
  > {
56
56
  constructor() {
57
57
  const headerBuffer = new Uint8Array(AdbPacketHeader.size);
58
58
  super({
59
59
  transform: async (chunk, controller) => {
60
- const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
61
- init.payloadLength = init.payload.byteLength;
60
+ await chunk.tryConsume(async (chunk) => {
61
+ const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
62
+ init.payloadLength = init.payload.length;
62
63
 
63
- AdbPacketHeader.serialize(init, headerBuffer);
64
- await controller.enqueue(headerBuffer);
64
+ await Consumable.ReadableStream.enqueue(
65
+ controller,
66
+ AdbPacketHeader.serialize(init, headerBuffer),
67
+ );
65
68
 
66
- if (init.payload.byteLength) {
67
- // USB protocol preserves packet boundaries,
68
- // so we must write payload separately as native ADB does,
69
- // otherwise the read operation on device will fail.
70
- await controller.enqueue(init.payload);
71
- }
69
+ if (init.payloadLength) {
70
+ // USB protocol preserves packet boundaries,
71
+ // so we must write payload separately as native ADB does,
72
+ // otherwise the read operation on device will fail.
73
+ await Consumable.ReadableStream.enqueue(
74
+ controller,
75
+ init.payload,
76
+ );
77
+ }
78
+ });
72
79
  },
73
80
  });
74
81
  }
@@ -1,19 +1,15 @@
1
1
  import { PromiseResolver } from "@yume-chan/async";
2
2
  import type { Disposable } from "@yume-chan/event";
3
3
  import type {
4
- Consumable,
5
4
  PushReadableStreamController,
6
5
  ReadableStream,
7
6
  WritableStream,
8
7
  WritableStreamDefaultController,
9
8
  } from "@yume-chan/stream-extra";
10
- import {
11
- ConsumableWritableStream,
12
- PushReadableStream,
13
- } from "@yume-chan/stream-extra";
9
+ import { MaybeConsumable, PushReadableStream } from "@yume-chan/stream-extra";
10
+ import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
14
11
 
15
12
  import type { AdbSocket } from "../adb.js";
16
- import { raceSignal } from "../server/index.js";
17
13
 
18
14
  import type { AdbPacketDispatcher } from "./dispatcher.js";
19
15
  import { AdbCommand } from "./packet.js";
@@ -26,11 +22,15 @@ export interface AdbDaemonSocketInfo {
26
22
  service: string;
27
23
  }
28
24
 
29
- export interface AdbDaemonSocketConstructionOptions
30
- extends AdbDaemonSocketInfo {
25
+ export interface AdbDaemonSocketInit extends AdbDaemonSocketInfo {
31
26
  dispatcher: AdbPacketDispatcher;
32
27
 
33
28
  highWaterMark?: number | undefined;
29
+
30
+ /**
31
+ * The initial delayed ack byte count, or `Infinity` if delayed ack is disabled.
32
+ */
33
+ availableWriteBytes: number;
34
34
  }
35
35
 
36
36
  export class AdbDaemonSocketController
@@ -49,9 +49,8 @@ export class AdbDaemonSocketController
49
49
  return this.#readable;
50
50
  }
51
51
 
52
- #writePromise: PromiseResolver<void> | undefined;
53
52
  #writableController!: WritableStreamDefaultController;
54
- readonly writable: WritableStream<Consumable<Uint8Array>>;
53
+ readonly writable: WritableStream<MaybeConsumable<Uint8Array>>;
55
54
 
56
55
  #closed = false;
57
56
 
@@ -65,7 +64,18 @@ export class AdbDaemonSocketController
65
64
  return this.#socket;
66
65
  }
67
66
 
68
- constructor(options: AdbDaemonSocketConstructionOptions) {
67
+ #availableWriteBytesChanged: PromiseResolver<void> | undefined;
68
+ /**
69
+ * When delayed ack is disabled, returns `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 message.
72
+ *
73
+ * When delayed ack is enabled, returns a non-negative finite number indicates the number of
74
+ * bytes that can be written to the socket before waiting for ack message.
75
+ */
76
+ #availableWriteBytes = 0;
77
+
78
+ constructor(options: AdbDaemonSocketInit) {
69
79
  this.#dispatcher = options.dispatcher;
70
80
  this.localId = options.localId;
71
81
  this.remoteId = options.remoteId;
@@ -76,11 +86,16 @@ export class AdbDaemonSocketController
76
86
  this.#readableController = controller;
77
87
  });
78
88
 
79
- this.writable = new ConsumableWritableStream<Uint8Array>({
89
+ this.writable = new MaybeConsumable.WritableStream<Uint8Array>({
80
90
  start: (controller) => {
81
91
  this.#writableController = controller;
92
+ controller.signal.addEventListener("abort", () => {
93
+ this.#availableWriteBytesChanged?.reject(
94
+ controller.signal.reason,
95
+ );
96
+ });
82
97
  },
83
- write: async (data, controller) => {
98
+ write: async (data) => {
84
99
  const size = data.length;
85
100
  const chunkSize = this.#dispatcher.options.maxPayloadSize;
86
101
  for (
@@ -88,23 +103,38 @@ export class AdbDaemonSocketController
88
103
  start < size;
89
104
  start = end, end += chunkSize
90
105
  ) {
91
- this.#writePromise = new PromiseResolver();
92
- await this.#dispatcher.sendPacket(
93
- AdbCommand.Write,
94
- this.localId,
95
- this.remoteId,
96
- data.subarray(start, end),
97
- );
98
- // Wait for ack packet
99
- await raceSignal(
100
- () => this.#writePromise!.promise,
101
- controller.signal,
102
- );
106
+ const chunk = data.subarray(start, end);
107
+ await this.#writeChunk(chunk);
103
108
  }
104
109
  },
105
110
  });
106
111
 
107
112
  this.#socket = new AdbDaemonSocket(this);
113
+ this.#availableWriteBytes = options.availableWriteBytes;
114
+ }
115
+
116
+ async #writeChunk(data: Uint8Array) {
117
+ const length = data.length;
118
+ while (this.#availableWriteBytes < length) {
119
+ // Only one lock is required because Web Streams API guarantees
120
+ // that `write` is not reentrant.
121
+ const resolver = new PromiseResolver<void>();
122
+ this.#availableWriteBytesChanged = resolver;
123
+ await resolver.promise;
124
+ }
125
+
126
+ if (this.#availableWriteBytes === Infinity) {
127
+ this.#availableWriteBytes = -1;
128
+ } else {
129
+ this.#availableWriteBytes -= length;
130
+ }
131
+
132
+ await this.#dispatcher.sendPacket(
133
+ AdbCommand.Write,
134
+ this.localId,
135
+ this.remoteId,
136
+ data,
137
+ );
108
138
  }
109
139
 
110
140
  async enqueue(data: Uint8Array) {
@@ -124,8 +154,9 @@ export class AdbDaemonSocketController
124
154
  }
125
155
  }
126
156
 
127
- ack() {
128
- this.#writePromise?.resolve();
157
+ public ack(bytes: number) {
158
+ this.#availableWriteBytes += bytes;
159
+ this.#availableWriteBytesChanged?.resolve();
129
160
  }
130
161
 
131
162
  async close(): Promise<void> {
@@ -134,6 +165,8 @@ export class AdbDaemonSocketController
134
165
  }
135
166
  this.#closed = true;
136
167
 
168
+ this.#availableWriteBytesChanged?.reject(new Error("Socket closed"));
169
+
137
170
  try {
138
171
  this.#writableController.error(new Error("Socket closed"));
139
172
  } catch {
@@ -144,6 +177,7 @@ export class AdbDaemonSocketController
144
177
  AdbCommand.Close,
145
178
  this.localId,
146
179
  this.remoteId,
180
+ EMPTY_UINT8_ARRAY,
147
181
  );
148
182
  }
149
183
 
@@ -185,7 +219,7 @@ export class AdbDaemonSocket implements AdbDaemonSocketInfo, AdbSocket {
185
219
  get readable(): ReadableStream<Uint8Array> {
186
220
  return this.#controller.readable;
187
221
  }
188
- get writable(): WritableStream<Consumable<Uint8Array>> {
222
+ get writable(): WritableStream<MaybeConsumable<Uint8Array>> {
189
223
  return this.#controller.writable;
190
224
  }
191
225