mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
doc(struct): update README
This commit is contained in:
parent
943aa96cab
commit
36d44243cc
25 changed files with 283 additions and 176 deletions
|
@ -533,13 +533,15 @@ class ScrcpyPageState {
|
|||
}
|
||||
};
|
||||
|
||||
stop() {
|
||||
async stop() {
|
||||
// Request to close client first
|
||||
await this.client?.close();
|
||||
this.client = undefined;
|
||||
|
||||
// Otherwise some packets may still arrive at decoder
|
||||
this.decoder?.dispose();
|
||||
this.decoder = undefined;
|
||||
|
||||
this.client?.close();
|
||||
this.client = undefined;
|
||||
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,16 +16,13 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<Uint8Array,
|
|||
public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) {
|
||||
this._readable = new ReadableStream<Uint8Array>({
|
||||
pull: async (controller) => {
|
||||
let result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
|
||||
|
||||
if (result.status === 'stall') {
|
||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/client/usb_osx.cpp#543
|
||||
await device.clearHalt('in', inEndpoint.endpointNumber);
|
||||
result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
|
||||
}
|
||||
|
||||
const view = result.data!;
|
||||
controller.enqueue(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
||||
const result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
|
||||
// `USBTransferResult` has three states: "ok", "stall" and "babble",
|
||||
// adbd on Android won't enter the "stall" (halt) state,
|
||||
// "ok" and "babble" both have received `data`,
|
||||
// "babble" just means there is more data to be read.
|
||||
// From spec, the `result.data` always covers the whole `buffer`.
|
||||
controller.enqueue(new Uint8Array(result.data!.buffer));
|
||||
},
|
||||
cancel: async () => {
|
||||
await device.close();
|
||||
|
@ -42,6 +39,9 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<Uint8Array,
|
|||
close: async () => {
|
||||
await device.close();
|
||||
},
|
||||
abort: async () => {
|
||||
await device.close();
|
||||
},
|
||||
}, {
|
||||
highWaterMark: 16 * 1024,
|
||||
size(chunk) { return chunk.byteLength; },
|
||||
|
|
|
@ -5,6 +5,8 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
|||
**WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
|
||||
|
||||
- [Compatibility](#compatibility)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Use without bundlers](#use-without-bundlers)
|
||||
- [Connection](#connection)
|
||||
- [Backend](#backend)
|
||||
- [`connect`](#connect)
|
||||
|
@ -36,18 +38,31 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
|
|||
|
||||
## Compatibility
|
||||
|
||||
This table only applies to this library itself. Specific backend may have different requirements.
|
||||
Here is a list of features, their used APIs, and their compatibilities. If you don't use an optional feature, you can ignore its requirement.
|
||||
|
||||
Some features can be polyfilled to support older runtimes, but this library doesn't ship with any polyfills.
|
||||
|
||||
Each backend may have different requirements.
|
||||
|
||||
### Basic usage
|
||||
|
||||
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------------------ | -------------------- |
|
||||
| Basic usage | 68 | 79 | 68 | No | 14<sup>1</sup>, 15 | 10.4<sup>2</sup>, 11 |
|
||||
| Use without bundlers<sup>3</sup> | 89 | 89 | 89 | No | 15 | 14.8 |
|
||||
| ------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
|
||||
| `@yume-chan/struct`<sup>1</sup> | 67 | 79 | 68 | No | 14 | 8.3<sup>2</sup>, 11 |
|
||||
| [Streams][MDN_Streams] | 67 | 79 | No | No | 14.1 | 16.5 |
|
||||
| *Overall* | 67 | 79 | No | No | 14.1 | 16.5 |
|
||||
|
||||
<sup>1</sup> Requires a polyfill for `DataView#getBigInt64`, `DataView#getBigUint64`, `DataView#setBigInt64` and `DataView#setBigUint64`
|
||||
<sup>1</sup> `uint64` and `string` used.
|
||||
|
||||
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Must be assigned to global object.
|
||||
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
|
||||
|
||||
<sup>3</sup> Because usage of Top-Level Await.
|
||||
### Use without bundlers
|
||||
|
||||
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| --------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||
| Top-level await<sup>1</sup> | 89 | 89 | 89 | No | 15 | 14.8 |
|
||||
|
||||
[MDN_Streams]: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
|
||||
|
||||
## Connection
|
||||
|
||||
|
|
|
@ -22,23 +22,25 @@ export function adbSyncPush(
|
|||
mtime: number = (Date.now() / 1000) | 0,
|
||||
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE,
|
||||
): WritableStream<Uint8Array> {
|
||||
// FIXME: `ChunkStream` can't forward `close` Promise.
|
||||
const { readable, writable } = new ChunkStream(packetSize);
|
||||
|
||||
readable.pipeTo(new WritableStream<Uint8Array>({
|
||||
async write(chunk) {
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk);
|
||||
}
|
||||
}));
|
||||
|
||||
return new WritableStream<Uint8Array>({
|
||||
async start() {
|
||||
const pathAndMode = `${filename},${mode.toString()}`;
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
|
||||
},
|
||||
async write(chunk) {
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk);
|
||||
await writable.getWriter().write(chunk);
|
||||
},
|
||||
async close() {
|
||||
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
|
||||
await adbSyncReadResponse(stream, ResponseTypes);
|
||||
}
|
||||
}, {
|
||||
highWaterMark: 16 * 1024,
|
||||
size(chunk) { return chunk.byteLength; }
|
||||
}));
|
||||
return writable;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
declare global {
|
||||
interface ArrayBuffer {
|
||||
// Disallow assigning `Arraybuffer` to `Uint8Array`
|
||||
__brand: never;
|
||||
}
|
||||
}
|
||||
|
||||
export * from './adb';
|
||||
export * from './auth';
|
||||
|
|
|
@ -52,6 +52,7 @@ export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uin
|
|||
controller.enqueue(AdbPacketHeader.serialize(packet));
|
||||
|
||||
if (packet.payloadLength) {
|
||||
// Enqueue payload separately to avoid copying
|
||||
controller.enqueue(packet.payload);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AsyncOperationManager } from '@yume-chan/async';
|
||||
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
||||
import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet';
|
||||
import { AbortController, ReadableStream, StructDeserializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../stream';
|
||||
import { AbortController, ReadableStream, StructDeserializeStream, WritableStream, WritableStreamDefaultWriter } from '../stream';
|
||||
import { decodeUtf8, encodeUtf8 } from '../utils';
|
||||
import { AdbSocketController } from './controller';
|
||||
import { AdbLogger } from './logger';
|
||||
|
@ -58,10 +58,9 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
|
||||
readable
|
||||
.pipeThrough(
|
||||
new TransformStream(),
|
||||
new StructDeserializeStream(AdbPacket),
|
||||
{ signal: this._abortController.signal, preventCancel: true }
|
||||
)
|
||||
.pipeThrough(new StructDeserializeStream(AdbPacket))
|
||||
.pipeTo(new WritableStream<AdbPacket>({
|
||||
write: async (packet) => {
|
||||
try {
|
||||
|
@ -98,12 +97,18 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
throw new Error(`Unhandled packet with command '${packet.command}'`);
|
||||
}
|
||||
} catch (e) {
|
||||
readable.cancel(e);
|
||||
writable.abort(e);
|
||||
this.errorEvent.fire(e as Error);
|
||||
|
||||
// Also stops the `writable`
|
||||
this._abortController.abort();
|
||||
|
||||
// Throw error here will stop the pipe
|
||||
// But won't close `readable` because of `preventCancel: true`
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}))
|
||||
.catch(() => { });
|
||||
|
||||
this._packetSerializeStream = new AdbPacketSerializeStream();
|
||||
this._packetSerializeStream.readable.pipeTo(
|
||||
|
|
|
@ -64,7 +64,7 @@ export class BufferedStream {
|
|||
}
|
||||
|
||||
array = new Uint8Array(length);
|
||||
array.set(value, 0);
|
||||
array.set(value);
|
||||
index = value.byteLength;
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,11 @@ export class BufferedStream {
|
|||
}
|
||||
|
||||
const { value } = result;
|
||||
if (value.byteLength === left) {
|
||||
array.set(value, index);
|
||||
return array;
|
||||
}
|
||||
|
||||
if (value.byteLength > left) {
|
||||
array.set(value.subarray(0, left), index);
|
||||
this.buffer = value.subarray(left);
|
||||
|
|
|
@ -318,8 +318,11 @@ async function* values(this: ReadableStream, options?: ReadableStreamIteratorOpt
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ('ReadableStream' in globalThis && 'WritableStream' in globalThis && 'TransformStream' in globalThis) {
|
||||
// This library can't use `@types/node` or `lib: dom`
|
||||
// because they will pollute the global scope
|
||||
// So `ReadableStream`, `WritableStream` and `TransformStream` are not available
|
||||
|
||||
if ('ReadableStream' in globalThis && 'WritableStream' in globalThis && 'TransformStream' in globalThis) {
|
||||
({
|
||||
ReadableStream,
|
||||
ReadableStreamDefaultController,
|
||||
|
@ -330,26 +333,36 @@ try {
|
|||
WritableStreamDefaultController,
|
||||
WritableStreamDefaultWriter,
|
||||
} = globalThis as any);
|
||||
} else {
|
||||
} else {
|
||||
try {
|
||||
// Node.js 16 has Web Streams types in `stream/web` module
|
||||
({
|
||||
// @ts-ignore
|
||||
ReadableStream,
|
||||
ReadableStreamDefaultController,
|
||||
ReadableStreamDefaultReader,
|
||||
// @ts-ignore
|
||||
TransformStream,
|
||||
TransformStreamDefaultController,
|
||||
// @ts-ignore
|
||||
WritableStream,
|
||||
WritableStreamDefaultController,
|
||||
WritableStreamDefaultWriter,
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
} = await import(/* webpackIgnore: true */ 'stream/web'));
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (!(Symbol.asyncIterator in ReadableStream.prototype)) {
|
||||
ReadableStream.prototype[Symbol.asyncIterator] = values;
|
||||
}
|
||||
if (!('values' in ReadableStream.prototype)) {
|
||||
ReadableStream.prototype.values = values;
|
||||
}
|
||||
} catch {
|
||||
// TODO: stream/detect: Load some polyfills
|
||||
|
||||
// @ts-ignore
|
||||
if (!ReadableStream || !WritableStream || !TransformStream) {
|
||||
throw new Error('Web Streams API is not available');
|
||||
}
|
||||
|
||||
if (!(Symbol.asyncIterator in ReadableStream.prototype)) {
|
||||
ReadableStream.prototype[Symbol.asyncIterator] = values;
|
||||
}
|
||||
if (!('values' in ReadableStream.prototype)) {
|
||||
ReadableStream.prototype.values = values;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<!--
|
||||
cspell: ignore Codecov
|
||||
cspell: ignore arraybufferuint8clampedarraystring
|
||||
cspell: ignore uint8arraystring
|
||||
-->
|
||||
|
||||

|
||||
|
@ -42,7 +42,7 @@ value.baz // string
|
|||
const buffer = MyStruct.serialize({
|
||||
foo: 42,
|
||||
bar: 42n,
|
||||
// `bazLength` automatically set to `baz.length`
|
||||
// `bazLength` automatically set to `baz`'s byte length
|
||||
baz: 'Hello, World!',
|
||||
});
|
||||
```
|
||||
|
@ -52,12 +52,15 @@ const buffer = MyStruct.serialize({
|
|||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [`int64`/`uint64`](#int64uint64)
|
||||
- [`string`](#string)
|
||||
- [API](#api)
|
||||
- [`placeholder`](#placeholder)
|
||||
- [`Struct`](#struct)
|
||||
- [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32)
|
||||
- [`int64`/`uint64`](#int64uint64)
|
||||
- [`arraybuffer`/`uint8ClampedArray`/`string`](#arraybufferuint8clampedarraystring)
|
||||
- [`int64`/`uint64`](#int64uint64-1)
|
||||
- [`uint8Array`/`string`](#uint8arraystring)
|
||||
- [`fields`](#fields)
|
||||
- [`extra`](#extra)
|
||||
- [`postDeserialize`](#postdeserialize)
|
||||
|
@ -65,6 +68,7 @@ const buffer = MyStruct.serialize({
|
|||
- [`serialize`](#serialize)
|
||||
- [Custom field type](#custom-field-type)
|
||||
- [`Struct#field`](#structfield)
|
||||
- [Relationship between types](#relationship-between-types)
|
||||
- [`StructFieldDefinition`](#structfielddefinition)
|
||||
- [`TValue`/`TOmitInitKey`](#tvaluetomitinitkey)
|
||||
- [`getSize`](#getsize)
|
||||
|
@ -79,25 +83,35 @@ const buffer = MyStruct.serialize({
|
|||
|
||||
## Compatibility
|
||||
|
||||
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| ------------------------------------------------------------ | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
|
||||
| **Basic usage** | 32 | 12 | 29 | 10<sup>1</sup> | 8 | 0.12 |
|
||||
| [`Promise`][MDN_Promise] | 32 | 12 | 29 | No<sup>1</sup> | 8 | 0.12 |
|
||||
| [`ArrayBuffer`][MDN_ArrayBuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
|
||||
| [`Uint8Array`][MDN_Uint8Array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
|
||||
| [`DataView`][MDN_DataView] | 9 | 12 | 15 | 10 | 5.1 | 0.10 |
|
||||
| **Use [`int64`/`uint64`](#int64uint64) type** | 67 | 79 | 68 | No<sup>2</sup> | 14 | 10.4 |
|
||||
| [`BigInt`][MDN_BigInt] | 67 | 79 | 68 | No<sup>2</sup> | 14 | 10.4 |
|
||||
| **Use [`string`](#arraybufferuint8clampedarraystring) type** | 38 | 79 | 29 | 10<sup>3</sup> | 10.1 | 8.3<sup>4</sup>, 11 |
|
||||
| [`TextEncoder`][MDN_TextEncoder] | 38 | 79 | 19 | No | 10.1 | 11 |
|
||||
Here is a list of features, their used APIs, and their compatibilities. If you don't use an optional feature, you can ignore its requirement.
|
||||
|
||||
<sup>1</sup> Requires a polyfill for Promise (e.g. [promise-polyfill](https://www.npmjs.com/package/promise-polyfill))
|
||||
Some features can be polyfilled to support older runtimes, but this library doesn't ship with any polyfills.
|
||||
|
||||
<sup>2</sup> `BigInt` can't be polyfilled
|
||||
### Basic usage
|
||||
|
||||
<sup>3</sup> Requires a polyfill for `TextEncoder` and `TextDecoder` (e.g. [fast-text-encoding](https://www.npmjs.com/package/fast-text-encoding))
|
||||
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||
| [`Promise`][MDN_Promise] | 32 | 12 | 29 | No | 8 | 0.12 |
|
||||
| [`ArrayBuffer`][MDN_ArrayBuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
|
||||
| [`Uint8Array`][MDN_Uint8Array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
|
||||
| [`DataView`][MDN_DataView] | 9 | 12 | 15 | 10 | 5.1 | 0.10 |
|
||||
| *Overall* | 32 | 12 | 29 | No | 8 | 0.12 |
|
||||
|
||||
<sup>4</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Must be assigned to `globalThis`.
|
||||
### [`int64`/`uint64`](#int64uint64-1)
|
||||
|
||||
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| ---------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------- |
|
||||
| [`BigInt`][MDN_BigInt]<sup>1</sup> | 67 | 79 | 68 | No | 14 | 10.4 |
|
||||
|
||||
<sup>1</sup> Can't be polyfilled
|
||||
|
||||
### [`string`](#uint8arraystring)
|
||||
|
||||
| API | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
|
||||
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
|
||||
| [`TextEncoder`][MDN_TextEncoder] | 38 | 79 | 19 | No | 10.1 | 8.3<sup>1</sup>, 11 |
|
||||
|
||||
<sup>1</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
|
||||
|
||||
[MDN_Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
[MDN_ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
|
||||
|
@ -161,7 +175,7 @@ class Struct<
|
|||
}
|
||||
```
|
||||
|
||||
Creates a new structure declaration.
|
||||
Creates a new structure definition.
|
||||
|
||||
<details>
|
||||
<summary>Generic parameters (click to expand)</summary>
|
||||
|
@ -169,7 +183,7 @@ Creates a new structure declaration.
|
|||
This information was added to help you understand how does it work. These are considered as "internal state" so don't specify them manually.
|
||||
|
||||
1. `TFields`: Type of the Struct value. Modified when new fields are added.
|
||||
2. `TOmitInitKey`: When serializing a structure containing variable length arrays, the length field can be calculate from the array field, so they doesn't need to be provided explicitly.
|
||||
2. `TOmitInitKey`: When serializing a structure containing variable length buffers, the length field can be calculate from the buffer field, so they doesn't need to be provided explicitly.
|
||||
3. `TExtra`: Type of extra fields. Modified when `extra` is called.
|
||||
4. `TPostDeserialized`: State of the `postDeserialize` function. Modified when `postDeserialize` is called. Affects return type of `deserialize`
|
||||
</details>
|
||||
|
@ -209,7 +223,7 @@ Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct
|
|||
|
||||
**Parameters**
|
||||
|
||||
1. `name`: (Required) Field name. Must have a [literal type](https://www.typescriptlang.org/docs/handbook/literal-types.html).
|
||||
1. `name`: (Required) Field name. Must be a string literal.
|
||||
2. `_typescriptType`: Set field's type. See examples below.
|
||||
|
||||
**Note**
|
||||
|
@ -272,19 +286,19 @@ int64<
|
|||
>;
|
||||
```
|
||||
|
||||
Appends an `int64`/`uint64` field to the `Struct`.
|
||||
Appends an `int64`/`uint64` field to the `Struct`. The usage is same as `uint32`/`uint32`.
|
||||
|
||||
Requires native support for `BigInt`. Check [compatibility table](#compatibility) for more information.
|
||||
|
||||
#### `arraybuffer`/`uint8ClampedArray`/`string`
|
||||
#### `uint8Array`/`string`
|
||||
|
||||
```ts
|
||||
arraybuffer<
|
||||
uint8Array<
|
||||
TName extends string | number | symbol,
|
||||
TTypeScriptType = ArrayBuffer
|
||||
>(
|
||||
name: TName,
|
||||
options: FixedLengthArrayBufferLikeFieldOptions,
|
||||
options: FixedLengthBufferLikeFieldOptions,
|
||||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TFields & Record<TName, TTypeScriptType>,
|
||||
|
@ -293,9 +307,10 @@ arraybuffer<
|
|||
TPostDeserialized
|
||||
>;
|
||||
|
||||
arraybuffer<
|
||||
uint8Array<
|
||||
TName extends string | number | symbol,
|
||||
TOptions extends VariableLengthArrayBufferLikeFieldOptions<TFields>,
|
||||
TLengthField extends LengthField<TFields>,
|
||||
TOptions extends VariableLengthBufferLikeFieldOptions<TFields, TLengthField>,
|
||||
TTypeScriptType = ArrayBuffer,
|
||||
>(
|
||||
name: TName,
|
||||
|
@ -303,20 +318,18 @@ arraybuffer<
|
|||
_typescriptType?: TTypeScriptType,
|
||||
): Struct<
|
||||
TFields & Record<TName, TTypeScriptType>,
|
||||
TOmitInitKey | TOptions['lengthField'],
|
||||
TOmitInitKey | TLengthField,
|
||||
TExtra,
|
||||
TPostDeserialized
|
||||
>;
|
||||
```
|
||||
|
||||
Appends an `ArrayBuffer`/`Uint8ClampedArray`/`string` field to the `Struct`.
|
||||
Appends an `uint8Array`/`string` field to the `Struct`.
|
||||
|
||||
The `options` parameter defines its length, it can be in two formats:
|
||||
|
||||
* `{ length: number }`: Presence of the `length` option indicates that it's a fixed length array.
|
||||
* `{ lengthField: string }`: Presence of the `lengthField` option indicates it's a variable length array. The `lengthField` options must refers to a `number` or `string` typed field that's already defined in this `Struct`. When deserializing, it will use that field's value as its length. And when serializing, it will write its length to that field.
|
||||
|
||||
All these three are actually deserialized to `ArrayBuffer`, then converted to `Uint8ClampedArray` or `string` for ease of use.
|
||||
* `{ lengthField: string; lengthFieldRadix?: number }`: Presence of the `lengthField` option indicates it's a variable length array. The `lengthField` options must refers to a `number` or `string` (can't be `bigint`) typed field that's already defined in this `Struct`. If the length field is a `string`, the optional `lengthFieldRadix` option (defaults to `10`) defines the radix when converting the string to a number. When deserializing, it will use that field's value as its length. When serializing, it will write its length to that field.
|
||||
|
||||
#### `fields`
|
||||
|
||||
|
@ -539,10 +552,20 @@ interface StructDeserializeStream {
|
|||
/**
|
||||
* Read data from the underlying data source.
|
||||
*
|
||||
* Stream must return exactly `length` bytes or data. If that's not possible
|
||||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an error.
|
||||
*/
|
||||
read(length: number): ArrayBuffer;
|
||||
read(length: number): Uint8Array;
|
||||
}
|
||||
|
||||
interface StructAsyncDeserializeStream {
|
||||
/**
|
||||
* Read data from the underlying data source.
|
||||
*
|
||||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an error.
|
||||
*/
|
||||
read(length: number): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
deserialize(
|
||||
|
@ -561,7 +584,9 @@ deserialize(
|
|||
>;
|
||||
```
|
||||
|
||||
Deserialize a Struct value from `stream`.
|
||||
Deserialize a struct value from `stream`.
|
||||
|
||||
It will be synchronous (returns a value) or asynchronous (returns a `Promise`) depending on the type of `stream`.
|
||||
|
||||
As the signature shows, if the `postDeserialize` callback returns any value, `deserialize` will return that value instead.
|
||||
|
||||
|
@ -570,16 +595,17 @@ The `read` method of `stream`, when being called, should returns exactly `length
|
|||
#### `serialize`
|
||||
|
||||
```ts
|
||||
serialize(
|
||||
init: Omit<TFields, TOmitInitKey>
|
||||
): ArrayBuffer;
|
||||
serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
|
||||
serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
|
||||
```
|
||||
|
||||
Serialize a Struct value into an `ArrayBuffer`.
|
||||
Serialize a struct value into an `Uint8Array`.
|
||||
|
||||
If an `output` is given, it will serialize the struct into it, and returns the number of bytes written.
|
||||
|
||||
## Custom field type
|
||||
|
||||
This library supports adding fields of user defined types.
|
||||
It's also possible to create your own field types.
|
||||
|
||||
### `Struct#field`
|
||||
|
||||
|
@ -600,7 +626,7 @@ field<
|
|||
|
||||
Appends a `StructFieldDefinition` to the `Struct`.
|
||||
|
||||
Actually, all built-in field type methods are aliases of `field`. For example, calling
|
||||
All built-in field type methods are actually aliases to it. For example, calling
|
||||
|
||||
```ts
|
||||
struct.int8('foo')
|
||||
|
@ -617,6 +643,15 @@ struct.field(
|
|||
)
|
||||
```
|
||||
|
||||
### Relationship between types
|
||||
|
||||
A `Struct` is a map between keys and `StructFieldDefinition`s.
|
||||
|
||||
A `StructValue` is a map between keys and `StructFieldValue`s.
|
||||
|
||||
A `Struct` can create (deserialize) multiple `StructValue`s with same field definitions.
|
||||
|
||||
Each time a `Struct` deserialize, each `StructFieldDefinition` in it creates exactly one `StructFieldValue` to be put into the `StructValue`.
|
||||
|
||||
### `StructFieldDefinition`
|
||||
|
||||
|
@ -632,13 +667,13 @@ abstract class StructFieldDefinition<
|
|||
}
|
||||
```
|
||||
|
||||
A `StructFieldDefinition` describes type, size and runtime semantics of a field.
|
||||
A field definition defines how to deserialize a field.
|
||||
|
||||
It's an `abstract` class, means it lacks some method implementations, so it shouldn't be constructed.
|
||||
It's an `abstract` class, means it can't be constructed (`new`ed) directly. It's only used as a base class for other field types.
|
||||
|
||||
#### `TValue`/`TOmitInitKey`
|
||||
|
||||
These two fields are used to provide type information to TypeScript. Their values will always be `undefined`, but having correct types is enough. You don't need to care about them.
|
||||
These two fields provide type information to TypeScript compiler. Their values will always be `undefined`, but having correct types is enough. You don't need to touch them.
|
||||
|
||||
#### `getSize`
|
||||
|
||||
|
@ -679,14 +714,17 @@ abstract deserialize(
|
|||
): Promise<StructFieldValue<this>>;
|
||||
```
|
||||
|
||||
Derived classes must implement this method to define how to deserialize a value from `stream`. Can also return a `Promise`.
|
||||
Derived classes must implement this method to define how to deserialize a value from `stream`.
|
||||
|
||||
It must be synchronous (returns a value) or asynchronous (returns a `Promise`) depending on the type of `stream`.
|
||||
|
||||
Usually implementations should be:
|
||||
|
||||
1. Somehow parse the value from `stream`
|
||||
2. Pass the value into its `create` method
|
||||
1. Read required bytes from `stream`
|
||||
2. Parse it to your type
|
||||
3. Pass the value into your own `create` method
|
||||
|
||||
Sometimes, some metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
|
||||
Sometimes, extra metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
|
||||
|
||||
### `StructFieldValue`
|
||||
|
||||
|
@ -696,9 +734,7 @@ abstract class StructFieldValue<
|
|||
>
|
||||
```
|
||||
|
||||
To define a custom type, one must create their own `StructFieldValue` type to define the runtime semantics.
|
||||
|
||||
Each `StructFieldValue` is linked to a `StructFieldDefinition`.
|
||||
A field value defines how to serialize a field.
|
||||
|
||||
#### `getSize`
|
||||
|
||||
|
@ -717,7 +753,7 @@ get(): TDefinition['TValue'];
|
|||
set(value: TDefinition['TValue']): void;
|
||||
```
|
||||
|
||||
Defines how to get or set this field's value. By default, it store its value in `value` field.
|
||||
Defines how to get or set this field's value. By default, it reads/writes its `value` field.
|
||||
|
||||
If one needs to manipulate other states when getting/setting values, they can override these methods.
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { ValueOrPromise } from '../utils';
|
||||
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
|
||||
import { StructFieldDefinition } from './definition';
|
||||
import type { StructFieldValue } from './field-value';
|
||||
import type { StructOptions } from './options';
|
||||
import type { StructAsyncDeserializeStream, StructDeserializeStream } from './stream';
|
||||
import type { StructValue } from './struct-value';
|
||||
|
||||
describe('StructFieldDefinition', () => {
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
|
||||
// cspell: ignore Syncbird
|
||||
|
||||
import type { StructAsyncDeserializeStream, StructDeserializeStream } from './stream';
|
||||
import type { StructFieldValue } from './field-value';
|
||||
import type { StructValue } from './struct-value';
|
||||
import type { StructOptions } from "./options";
|
||||
|
||||
/**
|
||||
* A field definition is a bridge between its type and its runtime value.
|
||||
*
|
||||
* `Struct` record fields in a list of `StructFieldDefinition`s.
|
||||
*
|
||||
* When `Struct#create` or `Struct#deserialize` are called, each field's definition
|
||||
* crates its own type of `StructFieldValue` to manage the field value in that `Struct` instance.
|
||||
*
|
||||
* One `StructFieldDefinition` can represents multiple similar types, just returns the corresponding
|
||||
* `StructFieldValue` when `createValue` was called.
|
||||
* A field definition defines how to deserialize a field.
|
||||
*
|
||||
* @template TOptions TypeScript type of this definition's `options`.
|
||||
* @template TValue TypeScript type of this field.
|
||||
|
@ -57,7 +52,10 @@ export abstract class StructFieldDefinition<
|
|||
): StructFieldValue<this>;
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, creates a `StructFieldValue` by parsing `context`.
|
||||
* When implemented in derived classes,It must be synchronous (returns a value) or asynchronous (returns a `Promise`) depending
|
||||
* on the type of `stream`. reads and creates a `StructFieldValue` from `stream`.
|
||||
*
|
||||
* `Syncbird` can be used to make the implementation easier.
|
||||
*/
|
||||
public abstract deserialize(
|
||||
options: Readonly<StructOptions>,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { ValueOrPromise } from '../utils';
|
||||
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
|
||||
import { StructFieldDefinition } from './definition';
|
||||
import { StructFieldValue } from './field-value';
|
||||
import type { StructOptions } from './options';
|
||||
import type { StructAsyncDeserializeStream, StructDeserializeStream } from './stream';
|
||||
import type { StructValue } from './struct-value';
|
||||
|
||||
describe('StructFieldValue', () => {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import type { StructOptions } from './context';
|
||||
import type { StructFieldDefinition } from './definition';
|
||||
import type { StructOptions } from './options';
|
||||
import type { StructValue } from './struct-value';
|
||||
|
||||
/**
|
||||
* Field runtime value manages one field of one `Struct` instance.
|
||||
* A field value defines how to serialize a field.
|
||||
*
|
||||
* If one `StructFieldDefinition` needs to change other field's semantics
|
||||
* It can override other fields' `StructFieldValue` in its own `StructFieldValue`'s constructor
|
||||
* It may contains extra metadata about the value which are essential or
|
||||
* helpful for the serialization process.
|
||||
*/
|
||||
export abstract class StructFieldValue<
|
||||
TDefinition extends StructFieldDefinition<any, any, any> = StructFieldDefinition<any, any, any>
|
||||
|
@ -44,14 +44,14 @@ export abstract class StructFieldValue<
|
|||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, returns the current value of this field
|
||||
* When implemented in derived classes, reads current field's value.
|
||||
*/
|
||||
public get(): TDefinition['TValue'] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* When implemented in derived classes, update the current value of this field
|
||||
* When implemented in derived classes, updates current field's value.
|
||||
*/
|
||||
public set(value: TDefinition['TValue']): void {
|
||||
this.value = value;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './context';
|
||||
export * from './definition';
|
||||
export * from './field-value';
|
||||
export * from './options';
|
||||
export * from './stream';
|
||||
export * from './struct-value';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { StructDefaultOptions } from './context';
|
||||
import { StructDefaultOptions } from './options';
|
||||
|
||||
describe('StructDefaultOptions', () => {
|
||||
describe('.littleEndian', () => {
|
19
libraries/struct/src/basic/options.ts
Normal file
19
libraries/struct/src/basic/options.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export interface StructOptions {
|
||||
/**
|
||||
* Whether all multi-byte fields in this struct are little-endian encoded.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
littleEndian: boolean;
|
||||
|
||||
// TODO: StructOptions: investigate whether this is necessary
|
||||
// I can't think about any other options which need to be struct wide.
|
||||
// Even endianness can be set on a per-field basis (because it's not meaningful
|
||||
// for some field types like `Uint8Array`, and very rarely, a struct may contain
|
||||
// mixed endianness).
|
||||
// It's just more common and a little more convenient to have it here.
|
||||
}
|
||||
|
||||
export const StructDefaultOptions: Readonly<StructOptions> = {
|
||||
littleEndian: false,
|
||||
};
|
|
@ -2,7 +2,7 @@ export interface StructDeserializeStream {
|
|||
/**
|
||||
* Read data from the underlying data source.
|
||||
*
|
||||
* Stream must return exactly `length` bytes or data. If that's not possible
|
||||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an error.
|
||||
*/
|
||||
read(length: number): Uint8Array;
|
||||
|
@ -12,21 +12,8 @@ export interface StructAsyncDeserializeStream {
|
|||
/**
|
||||
* Read data from the underlying data source.
|
||||
*
|
||||
* Context must return exactly `length` bytes or data. If that's not possible
|
||||
* The stream must return exactly `length` bytes or data. If that's not possible
|
||||
* (due to end of file or other error condition), it must throw an error.
|
||||
*/
|
||||
read(length: number): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface StructOptions {
|
||||
/**
|
||||
* Whether all multi-byte fields in this struct are little-endian encoded.
|
||||
*
|
||||
* Default to `false`
|
||||
*/
|
||||
littleEndian: boolean;
|
||||
}
|
||||
|
||||
export const StructDefaultOptions: Readonly<StructOptions> = {
|
||||
littleEndian: false,
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import type { StructFieldValue } from './field-value';
|
||||
|
||||
/**
|
||||
* Manages the initialization process of a struct value
|
||||
* A struct value is a map between keys in a struct and their field values.
|
||||
*/
|
||||
export class StructValue {
|
||||
/** @internal */ readonly fieldValues: Record<PropertyKey, StructFieldValue> = {};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
declare global {
|
||||
interface ArrayBuffer {
|
||||
// Disallow assigning `Arraybuffer` to `Uint8Array`
|
||||
// Disallow assigning `Uint8Array` to `Arraybuffer`
|
||||
__brand: never;
|
||||
}
|
||||
|
||||
|
|
|
@ -526,6 +526,9 @@ export class Struct<
|
|||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a struct value from `stream`.
|
||||
*/
|
||||
public deserialize(
|
||||
stream: StructDeserializeStream,
|
||||
): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
|
||||
|
|
|
@ -160,6 +160,8 @@ export class BufferLikeFieldValue<
|
|||
|
||||
public override set(value: TDefinition['TValue']): void {
|
||||
super.set(value);
|
||||
// When value changes, clear the cached `array`
|
||||
// It will be lazily calculated in `serialize()`
|
||||
this.array = undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BufferFieldSubType, BufferLikeFieldDefinition } from './base';
|
||||
import { BufferLikeFieldDefinition, type BufferFieldSubType } from './base';
|
||||
|
||||
export interface FixedLengthBufferLikeFieldOptions {
|
||||
length: number;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from '../../basic';
|
||||
import { StructFieldValue, type StructFieldDefinition, type StructOptions, type StructValue } from '../../basic';
|
||||
import type { KeysOfType } from '../../utils';
|
||||
import { BufferFieldSubType, BufferLikeFieldDefinition, BufferLikeFieldValue } from './base';
|
||||
import { BufferLikeFieldDefinition, BufferLikeFieldValue, type BufferFieldSubType } from './base';
|
||||
|
||||
export type LengthField<TFields> = KeysOfType<TFields, number | string>;
|
||||
|
||||
|
@ -8,9 +8,20 @@ export interface VariableLengthBufferLikeFieldOptions<
|
|||
TFields = object,
|
||||
TLengthField extends LengthField<TFields> = any,
|
||||
> {
|
||||
/**
|
||||
* The name of the field that contains the length of the buffer.
|
||||
*
|
||||
* This field must be a `number` or `string` (can't be `bigint`) field.
|
||||
*/
|
||||
lengthField: TLengthField;
|
||||
|
||||
lengthFieldBase?: number;
|
||||
/**
|
||||
* If the `lengthField` refers to a string field,
|
||||
* what radix to use when converting the string to a number.
|
||||
*
|
||||
* @default 10
|
||||
*/
|
||||
lengthFieldRadix?: number;
|
||||
}
|
||||
|
||||
export class VariableLengthBufferLikeFieldDefinition<
|
||||
|
@ -28,7 +39,7 @@ export class VariableLengthBufferLikeFieldDefinition<
|
|||
protected override getDeserializeSize(struct: StructValue) {
|
||||
let value = struct.value[this.options.lengthField] as number | string;
|
||||
if (typeof value === 'string') {
|
||||
value = Number.parseInt(value, this.options.lengthFieldBase ?? 10);
|
||||
value = Number.parseInt(value, this.options.lengthFieldRadix ?? 10);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
@ -127,13 +138,16 @@ export class VariableLengthBufferLikeFieldLengthValue
|
|||
|
||||
const originalValue = this.originalField.get();
|
||||
if (typeof originalValue === 'string') {
|
||||
value = value.toString(this.arrayBufferField.definition.options.lengthFieldBase ?? 10);
|
||||
value = value.toString(this.arrayBufferField.definition.options.lengthFieldRadix ?? 10);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override set() { }
|
||||
public override set() {
|
||||
// Ignore setting
|
||||
// It will always be in sync with the buffer size
|
||||
}
|
||||
|
||||
serialize(dataView: DataView, offset: number) {
|
||||
this.originalField.set(this.get());
|
||||
|
|
|
@ -48,9 +48,17 @@ export function placeholder<T>(): T {
|
|||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
// @ts-expect-error @types/node missing `TextEncoder`
|
||||
// This library can't use `@types/node` or `lib: dom`
|
||||
// because they will pollute the global scope
|
||||
// So `TextEncoder` and `TextDecoder` are not available
|
||||
|
||||
// Node.js 8.3 ships `TextEncoder` and `TextDecoder` in `util` module.
|
||||
// But using top level await to load them requires Node.js 14.1.
|
||||
// So there is no point to do that. Let's just assume they exists in global.
|
||||
|
||||
// @ts-expect-error
|
||||
const Utf8Encoder = new TextEncoder();
|
||||
// @ts-expect-error @types/node missing `TextDecoder`
|
||||
// @ts-expect-error
|
||||
const Utf8Decoder = new TextDecoder();
|
||||
|
||||
export function encodeUtf8(input: string): Uint8Array {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue