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

View file

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

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.
- [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

View file

@ -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;
},
});
}

View file

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

View file

@ -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);
}
},

View file

@ -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(

View file

@ -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);

View file

@ -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;
}

View file

@ -2,7 +2,7 @@
<!--
cspell: ignore Codecov
cspell: ignore arraybufferuint8clampedarraystring
cspell: ignore uint8arraystring
-->
![license](https://img.shields.io/npm/l/@yume-chan/struct)
@ -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 |
| &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 |
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.

View file

@ -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', () => {

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 { 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>,

View file

@ -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', () => {

View file

@ -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;

View file

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

View file

@ -1,4 +1,4 @@
import { StructDefaultOptions } from './context';
import { StructDefaultOptions } from './options';
describe('StructDefaultOptions', () => {
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.
*
* 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,
};

View file

@ -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> = {};

View file

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

View file

@ -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>;

View file

@ -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;
}

View file

@ -1,4 +1,4 @@
import { BufferFieldSubType, BufferLikeFieldDefinition } from './base';
import { BufferLikeFieldDefinition, type BufferFieldSubType } from './base';
export interface FixedLengthBufferLikeFieldOptions {
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 { 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());

View file

@ -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 {