doc(struct): update README

This commit is contained in:
Simon Chan 2022-02-24 18:25:07 +08:00
parent 943aa96cab
commit 36d44243cc
25 changed files with 283 additions and 176 deletions

View file

@ -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?.dispose();
this.decoder = undefined; this.decoder = undefined;
this.client?.close();
this.client = undefined;
this.running = false; this.running = false;
} }

View file

@ -16,16 +16,13 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<Uint8Array,
public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) { public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) {
this._readable = new ReadableStream<Uint8Array>({ this._readable = new ReadableStream<Uint8Array>({
pull: async (controller) => { pull: async (controller) => {
let result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize); const result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
// `USBTransferResult` has three states: "ok", "stall" and "babble",
if (result.status === 'stall') { // adbd on Android won't enter the "stall" (halt) state,
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/client/usb_osx.cpp#543 // "ok" and "babble" both have received `data`,
await device.clearHalt('in', inEndpoint.endpointNumber); // "babble" just means there is more data to be read.
result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize); // From spec, the `result.data` always covers the whole `buffer`.
} controller.enqueue(new Uint8Array(result.data!.buffer));
const view = result.data!;
controller.enqueue(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
}, },
cancel: async () => { cancel: async () => {
await device.close(); await device.close();
@ -42,6 +39,9 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<Uint8Array,
close: async () => { close: async () => {
await device.close(); await device.close();
}, },
abort: async () => {
await device.close();
},
}, { }, {
highWaterMark: 16 * 1024, highWaterMark: 16 * 1024,
size(chunk) { return chunk.byteLength; }, size(chunk) { return chunk.byteLength; },

View file

@ -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. **WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
- [Compatibility](#compatibility) - [Compatibility](#compatibility)
- [Basic usage](#basic-usage)
- [Use without bundlers](#use-without-bundlers)
- [Connection](#connection) - [Connection](#connection)
- [Backend](#backend) - [Backend](#backend)
- [`connect`](#connect) - [`connect`](#connect)
@ -36,18 +38,31 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
## Compatibility ## 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.
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js | Some features can be polyfilled to support older runtimes, but this library doesn't ship with any polyfills.
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------------------ | -------------------- |
| 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 |
<sup>1</sup> Requires a polyfill for `DataView#getBigInt64`, `DataView#getBigUint64`, `DataView#setBigInt64` and `DataView#setBigUint64` Each backend may have different requirements.
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Must be assigned to global object. ### Basic usage
<sup>3</sup> Because usage of Top-Level Await. | | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
| ------------------------------- | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
| `@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> `uint64` and `string` used.
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Need to be assigned to `globalThis`.
### 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 ## Connection

View file

@ -22,23 +22,25 @@ export function adbSyncPush(
mtime: number = (Date.now() / 1000) | 0, mtime: number = (Date.now() / 1000) | 0,
packetSize: number = ADB_SYNC_MAX_PACKET_SIZE, packetSize: number = ADB_SYNC_MAX_PACKET_SIZE,
): WritableStream<Uint8Array> { ): WritableStream<Uint8Array> {
// FIXME: `ChunkStream` can't forward `close` Promise.
const { readable, writable } = new ChunkStream(packetSize); const { readable, writable } = new ChunkStream(packetSize);
readable.pipeTo(new WritableStream<Uint8Array>({ readable.pipeTo(new WritableStream<Uint8Array>({
async write(chunk) {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk);
}
}));
return new WritableStream<Uint8Array>({
async start() { async start() {
const pathAndMode = `${filename},${mode.toString()}`; const pathAndMode = `${filename},${mode.toString()}`;
await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode); await adbSyncWriteRequest(writer, AdbSyncRequestId.Send, pathAndMode);
}, },
async write(chunk) { async write(chunk) {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Data, chunk); await writable.getWriter().write(chunk);
}, },
async close() { async close() {
await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime); await adbSyncWriteRequest(writer, AdbSyncRequestId.Done, mtime);
await adbSyncReadResponse(stream, ResponseTypes); await adbSyncReadResponse(stream, ResponseTypes);
} },
}, { });
highWaterMark: 16 * 1024,
size(chunk) { return chunk.byteLength; }
}));
return writable;
} }

View file

@ -1,9 +1,3 @@
declare global {
interface ArrayBuffer {
// Disallow assigning `Arraybuffer` to `Uint8Array`
__brand: never;
}
}
export * from './adb'; export * from './adb';
export * from './auth'; export * from './auth';

View file

@ -52,6 +52,7 @@ export class AdbPacketSerializeStream extends TransformStream<AdbPacketInit, Uin
controller.enqueue(AdbPacketHeader.serialize(packet)); controller.enqueue(AdbPacketHeader.serialize(packet));
if (packet.payloadLength) { if (packet.payloadLength) {
// Enqueue payload separately to avoid copying
controller.enqueue(packet.payload); controller.enqueue(packet.payload);
} }
}, },

View file

@ -1,7 +1,7 @@
import { AsyncOperationManager } from '@yume-chan/async'; import { AsyncOperationManager } from '@yume-chan/async';
import { AutoDisposable, EventEmitter } from '@yume-chan/event'; import { AutoDisposable, EventEmitter } from '@yume-chan/event';
import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet'; 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 { decodeUtf8, encodeUtf8 } from '../utils';
import { AdbSocketController } from './controller'; import { AdbSocketController } from './controller';
import { AdbLogger } from './logger'; import { AdbLogger } from './logger';
@ -58,10 +58,9 @@ export class AdbPacketDispatcher extends AutoDisposable {
readable readable
.pipeThrough( .pipeThrough(
new TransformStream(), new StructDeserializeStream(AdbPacket),
{ signal: this._abortController.signal, preventCancel: true } { signal: this._abortController.signal, preventCancel: true }
) )
.pipeThrough(new StructDeserializeStream(AdbPacket))
.pipeTo(new WritableStream<AdbPacket>({ .pipeTo(new WritableStream<AdbPacket>({
write: async (packet) => { write: async (packet) => {
try { try {
@ -98,12 +97,18 @@ export class AdbPacketDispatcher extends AutoDisposable {
throw new Error(`Unhandled packet with command '${packet.command}'`); throw new Error(`Unhandled packet with command '${packet.command}'`);
} }
} catch (e) { } catch (e) {
readable.cancel(e);
writable.abort(e);
this.errorEvent.fire(e as Error); 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 = new AdbPacketSerializeStream();
this._packetSerializeStream.readable.pipeTo( this._packetSerializeStream.readable.pipeTo(

View file

@ -64,7 +64,7 @@ export class BufferedStream {
} }
array = new Uint8Array(length); array = new Uint8Array(length);
array.set(value, 0); array.set(value);
index = value.byteLength; index = value.byteLength;
} }
@ -81,6 +81,11 @@ export class BufferedStream {
} }
const { value } = result; const { value } = result;
if (value.byteLength === left) {
array.set(value, index);
return array;
}
if (value.byteLength > left) { if (value.byteLength > left) {
array.set(value.subarray(0, left), index); array.set(value.subarray(0, left), index);
this.buffer = value.subarray(left); this.buffer = value.subarray(left);

View file

@ -318,38 +318,51 @@ async function* values(this: ReadableStream, options?: ReadableStreamIteratorOpt
} }
} }
try { // This library can't use `@types/node` or `lib: dom`
if ('ReadableStream' in globalThis && 'WritableStream' in globalThis && 'TransformStream' in globalThis) { // because they will pollute the global scope
({ // So `ReadableStream`, `WritableStream` and `TransformStream` are not available
ReadableStream,
ReadableStreamDefaultController,
ReadableStreamDefaultReader,
TransformStream,
TransformStreamDefaultController,
WritableStream,
WritableStreamDefaultController,
WritableStreamDefaultWriter,
} = globalThis as any);
} else {
({
ReadableStream,
ReadableStreamDefaultController,
ReadableStreamDefaultReader,
TransformStream,
TransformStreamDefaultController,
WritableStream,
WritableStreamDefaultController,
WritableStreamDefaultWriter,
// @ts-expect-error
} = await import(/* webpackIgnore: true */ 'stream/web'));
}
if (!(Symbol.asyncIterator in ReadableStream.prototype)) { if ('ReadableStream' in globalThis && 'WritableStream' in globalThis && 'TransformStream' in globalThis) {
ReadableStream.prototype[Symbol.asyncIterator] = values; ({
} ReadableStream,
if (!('values' in ReadableStream.prototype)) { ReadableStreamDefaultController,
ReadableStream.prototype.values = values; ReadableStreamDefaultReader,
} TransformStream,
} catch { TransformStreamDefaultController,
WritableStream,
WritableStreamDefaultController,
WritableStreamDefaultWriter,
} = globalThis as any);
} 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-ignore
} = await import(/* webpackIgnore: true */ 'stream/web'));
} catch { }
}
// TODO: stream/detect: Load some polyfills
// @ts-ignore
if (!ReadableStream || !WritableStream || !TransformStream) {
throw new Error('Web Streams API is not available'); 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;
}

View file

@ -2,7 +2,7 @@
<!-- <!--
cspell: ignore Codecov cspell: ignore Codecov
cspell: ignore arraybufferuint8clampedarraystring cspell: ignore uint8arraystring
--> -->
![license](https://img.shields.io/npm/l/@yume-chan/struct) ![license](https://img.shields.io/npm/l/@yume-chan/struct)
@ -42,7 +42,7 @@ value.baz // string
const buffer = MyStruct.serialize({ const buffer = MyStruct.serialize({
foo: 42, foo: 42,
bar: 42n, bar: 42n,
// `bazLength` automatically set to `baz.length` // `bazLength` automatically set to `baz`'s byte length
baz: 'Hello, World!', baz: 'Hello, World!',
}); });
``` ```
@ -52,12 +52,15 @@ const buffer = MyStruct.serialize({
- [Installation](#installation) - [Installation](#installation)
- [Quick Start](#quick-start) - [Quick Start](#quick-start)
- [Compatibility](#compatibility) - [Compatibility](#compatibility)
- [Basic usage](#basic-usage)
- [`int64`/`uint64`](#int64uint64)
- [`string`](#string)
- [API](#api) - [API](#api)
- [`placeholder`](#placeholder) - [`placeholder`](#placeholder)
- [`Struct`](#struct) - [`Struct`](#struct)
- [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32) - [`int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32`](#int8uint8int16uint16int32uint32)
- [`int64`/`uint64`](#int64uint64) - [`int64`/`uint64`](#int64uint64-1)
- [`arraybuffer`/`uint8ClampedArray`/`string`](#arraybufferuint8clampedarraystring) - [`uint8Array`/`string`](#uint8arraystring)
- [`fields`](#fields) - [`fields`](#fields)
- [`extra`](#extra) - [`extra`](#extra)
- [`postDeserialize`](#postdeserialize) - [`postDeserialize`](#postdeserialize)
@ -65,6 +68,7 @@ const buffer = MyStruct.serialize({
- [`serialize`](#serialize) - [`serialize`](#serialize)
- [Custom field type](#custom-field-type) - [Custom field type](#custom-field-type)
- [`Struct#field`](#structfield) - [`Struct#field`](#structfield)
- [Relationship between types](#relationship-between-types)
- [`StructFieldDefinition`](#structfielddefinition) - [`StructFieldDefinition`](#structfielddefinition)
- [`TValue`/`TOmitInitKey`](#tvaluetomitinitkey) - [`TValue`/`TOmitInitKey`](#tvaluetomitinitkey)
- [`getSize`](#getsize) - [`getSize`](#getsize)
@ -79,25 +83,35 @@ const buffer = MyStruct.serialize({
## Compatibility ## Compatibility
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js | 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.
| ------------------------------------------------------------ | ------ | ---- | ------- | ----------------- | ------ | ------------------- |
| **Basic usage** | 32 | 12 | 29 | 10<sup>1</sup> | 8 | 0.12 |
| &nbsp;&nbsp;&nbsp;&nbsp;[`Promise`][MDN_Promise] | 32 | 12 | 29 | No<sup>1</sup> | 8 | 0.12 |
| &nbsp;&nbsp;&nbsp;&nbsp;[`ArrayBuffer`][MDN_ArrayBuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
| &nbsp;&nbsp;&nbsp;&nbsp;[`Uint8Array`][MDN_Uint8Array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
| &nbsp;&nbsp;&nbsp;&nbsp;[`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 |
| &nbsp;&nbsp;&nbsp;&nbsp;[`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 |
| &nbsp;&nbsp;&nbsp;&nbsp;[`TextEncoder`][MDN_TextEncoder] | 38 | 79 | 19 | No | 10.1 | 11 |
<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_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 [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> <details>
<summary>Generic parameters (click to expand)</summary> <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. 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. 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. 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` 4. `TPostDeserialized`: State of the `postDeserialize` function. Modified when `postDeserialize` is called. Affects return type of `deserialize`
</details> </details>
@ -209,7 +223,7 @@ Appends an `int8`/`uint8`/`int16`/`uint16`/`int32`/`uint32` field to the `Struct
**Parameters** **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. 2. `_typescriptType`: Set field's type. See examples below.
**Note** **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. Requires native support for `BigInt`. Check [compatibility table](#compatibility) for more information.
#### `arraybuffer`/`uint8ClampedArray`/`string` #### `uint8Array`/`string`
```ts ```ts
arraybuffer< uint8Array<
TName extends string | number | symbol, TName extends string | number | symbol,
TTypeScriptType = ArrayBuffer TTypeScriptType = ArrayBuffer
>( >(
name: TName, name: TName,
options: FixedLengthArrayBufferLikeFieldOptions, options: FixedLengthBufferLikeFieldOptions,
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
): Struct< ): Struct<
TFields & Record<TName, TTypeScriptType>, TFields & Record<TName, TTypeScriptType>,
@ -293,9 +307,10 @@ arraybuffer<
TPostDeserialized TPostDeserialized
>; >;
arraybuffer< uint8Array<
TName extends string | number | symbol, TName extends string | number | symbol,
TOptions extends VariableLengthArrayBufferLikeFieldOptions<TFields>, TLengthField extends LengthField<TFields>,
TOptions extends VariableLengthBufferLikeFieldOptions<TFields, TLengthField>,
TTypeScriptType = ArrayBuffer, TTypeScriptType = ArrayBuffer,
>( >(
name: TName, name: TName,
@ -303,20 +318,18 @@ arraybuffer<
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
): Struct< ): Struct<
TFields & Record<TName, TTypeScriptType>, TFields & Record<TName, TTypeScriptType>,
TOmitInitKey | TOptions['lengthField'], TOmitInitKey | TLengthField,
TExtra, TExtra,
TPostDeserialized 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: 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. * `{ 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. * `{ 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.
All these three are actually deserialized to `ArrayBuffer`, then converted to `Uint8ClampedArray` or `string` for ease of use.
#### `fields` #### `fields`
@ -539,10 +552,20 @@ interface StructDeserializeStream {
/** /**
* Read data from the underlying data source. * 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. * (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( 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. 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` #### `serialize`
```ts ```ts
serialize( serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
init: Omit<TFields, TOmitInitKey> serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
): ArrayBuffer;
``` ```
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 ## Custom field type
This library supports adding fields of user defined types. It's also possible to create your own field types.
### `Struct#field` ### `Struct#field`
@ -600,7 +626,7 @@ field<
Appends a `StructFieldDefinition` to the `Struct`. 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 ```ts
struct.int8('foo') 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` ### `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` #### `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` #### `getSize`
@ -679,14 +714,17 @@ abstract deserialize(
): Promise<StructFieldValue<this>>; ): 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: Usually implementations should be:
1. Somehow parse the value from `stream` 1. Read required bytes from `stream`
2. Pass the value into its `create` method 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` ### `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. A field value defines how to serialize a field.
Each `StructFieldValue` is linked to a `StructFieldDefinition`.
#### `getSize` #### `getSize`
@ -717,7 +753,7 @@ get(): TDefinition['TValue'];
set(value: TDefinition['TValue']): void; 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. If one needs to manipulate other states when getting/setting values, they can override these methods.

View file

@ -1,7 +1,8 @@
import type { ValueOrPromise } from '../utils'; import type { ValueOrPromise } from '../utils';
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
import { StructFieldDefinition } from './definition'; import { StructFieldDefinition } from './definition';
import type { StructFieldValue } from './field-value'; import type { StructFieldValue } from './field-value';
import type { StructOptions } from './options';
import type { StructAsyncDeserializeStream, StructDeserializeStream } from './stream';
import type { StructValue } from './struct-value'; import type { StructValue } from './struct-value';
describe('StructFieldDefinition', () => { describe('StructFieldDefinition', () => {

View file

@ -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 { StructFieldValue } from './field-value';
import type { StructValue } from './struct-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. * A field definition defines how to deserialize a field.
*
* `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.
* *
* @template TOptions TypeScript type of this definition's `options`. * @template TOptions TypeScript type of this definition's `options`.
* @template TValue TypeScript type of this field. * @template TValue TypeScript type of this field.
@ -57,7 +52,10 @@ export abstract class StructFieldDefinition<
): StructFieldValue<this>; ): 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( public abstract deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,

View file

@ -1,7 +1,8 @@
import type { ValueOrPromise } from '../utils'; import type { ValueOrPromise } from '../utils';
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
import { StructFieldDefinition } from './definition'; import { StructFieldDefinition } from './definition';
import { StructFieldValue } from './field-value'; import { StructFieldValue } from './field-value';
import type { StructOptions } from './options';
import type { StructAsyncDeserializeStream, StructDeserializeStream } from './stream';
import type { StructValue } from './struct-value'; import type { StructValue } from './struct-value';
describe('StructFieldValue', () => { describe('StructFieldValue', () => {

View file

@ -1,12 +1,12 @@
import type { StructOptions } from './context';
import type { StructFieldDefinition } from './definition'; import type { StructFieldDefinition } from './definition';
import type { StructOptions } from './options';
import type { StructValue } from './struct-value'; 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 may contains extra metadata about the value which are essential or
* It can override other fields' `StructFieldValue` in its own `StructFieldValue`'s constructor * helpful for the serialization process.
*/ */
export abstract class StructFieldValue< export abstract class StructFieldValue<
TDefinition extends StructFieldDefinition<any, any, any> = StructFieldDefinition<any, any, any> 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'] { public get(): TDefinition['TValue'] {
return this.value; 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 { public set(value: TDefinition['TValue']): void {
this.value = value; this.value = value;

View file

@ -1,4 +1,5 @@
export * from './context';
export * from './definition'; export * from './definition';
export * from './field-value'; export * from './field-value';
export * from './options';
export * from './stream';
export * from './struct-value'; export * from './struct-value';

View file

@ -1,4 +1,4 @@
import { StructDefaultOptions } from './context'; import { StructDefaultOptions } from './options';
describe('StructDefaultOptions', () => { describe('StructDefaultOptions', () => {
describe('.littleEndian', () => { describe('.littleEndian', () => {

View 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,
};

View file

@ -2,7 +2,7 @@ export interface StructDeserializeStream {
/** /**
* Read data from the underlying data source. * 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. * (due to end of file or other error condition), it must throw an error.
*/ */
read(length: number): Uint8Array; read(length: number): Uint8Array;
@ -12,21 +12,8 @@ export interface StructAsyncDeserializeStream {
/** /**
* Read data from the underlying data source. * 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. * (due to end of file or other error condition), it must throw an error.
*/ */
read(length: number): Promise<Uint8Array>; 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,
};

View file

@ -1,7 +1,7 @@
import type { StructFieldValue } from './field-value'; 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 { export class StructValue {
/** @internal */ readonly fieldValues: Record<PropertyKey, StructFieldValue> = {}; /** @internal */ readonly fieldValues: Record<PropertyKey, StructFieldValue> = {};

View file

@ -1,6 +1,6 @@
declare global { declare global {
interface ArrayBuffer { interface ArrayBuffer {
// Disallow assigning `Arraybuffer` to `Uint8Array` // Disallow assigning `Uint8Array` to `Arraybuffer`
__brand: never; __brand: never;
} }

View file

@ -526,6 +526,9 @@ export class Struct<
return this as any; return this as any;
} }
/**
* Deserialize a struct value from `stream`.
*/
public deserialize( public deserialize(
stream: StructDeserializeStream, stream: StructDeserializeStream,
): StructDeserializedResult<TFields, TExtra, TPostDeserialized>; ): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;

View file

@ -160,6 +160,8 @@ export class BufferLikeFieldValue<
public override set(value: TDefinition['TValue']): void { public override set(value: TDefinition['TValue']): void {
super.set(value); super.set(value);
// When value changes, clear the cached `array`
// It will be lazily calculated in `serialize()`
this.array = undefined; this.array = undefined;
} }

View file

@ -1,4 +1,4 @@
import { BufferFieldSubType, BufferLikeFieldDefinition } from './base'; import { BufferLikeFieldDefinition, type BufferFieldSubType } from './base';
export interface FixedLengthBufferLikeFieldOptions { export interface FixedLengthBufferLikeFieldOptions {
length: number; length: number;

View file

@ -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 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>; export type LengthField<TFields> = KeysOfType<TFields, number | string>;
@ -8,9 +8,20 @@ export interface VariableLengthBufferLikeFieldOptions<
TFields = object, TFields = object,
TLengthField extends LengthField<TFields> = any, 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; 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< export class VariableLengthBufferLikeFieldDefinition<
@ -28,7 +39,7 @@ export class VariableLengthBufferLikeFieldDefinition<
protected override getDeserializeSize(struct: StructValue) { protected override getDeserializeSize(struct: StructValue) {
let value = struct.value[this.options.lengthField] as number | string; let value = struct.value[this.options.lengthField] as number | string;
if (typeof value === 'string') { if (typeof value === 'string') {
value = Number.parseInt(value, this.options.lengthFieldBase ?? 10); value = Number.parseInt(value, this.options.lengthFieldRadix ?? 10);
} }
return value; return value;
} }
@ -127,13 +138,16 @@ export class VariableLengthBufferLikeFieldLengthValue
const originalValue = this.originalField.get(); const originalValue = this.originalField.get();
if (typeof originalValue === 'string') { if (typeof originalValue === 'string') {
value = value.toString(this.arrayBufferField.definition.options.lengthFieldBase ?? 10); value = value.toString(this.arrayBufferField.definition.options.lengthFieldRadix ?? 10);
} }
return value; 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) { serialize(dataView: DataView, offset: number) {
this.originalField.set(this.get()); this.originalField.set(this.get());

View file

@ -48,9 +48,17 @@ export function placeholder<T>(): T {
return undefined as unknown as 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(); const Utf8Encoder = new TextEncoder();
// @ts-expect-error @types/node missing `TextDecoder` // @ts-expect-error
const Utf8Decoder = new TextDecoder(); const Utf8Decoder = new TextDecoder();
export function encodeUtf8(input: string): Uint8Array { export function encodeUtf8(input: string): Uint8Array {