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?.dispose();
|
||||||
this.decoder = undefined;
|
this.decoder = undefined;
|
||||||
|
|
||||||
this.client?.close();
|
|
||||||
this.client = undefined;
|
|
||||||
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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; },
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
cspell: ignore Codecov
|
cspell: ignore Codecov
|
||||||
cspell: ignore arraybufferuint8clampedarraystring
|
cspell: ignore uint8arraystring
|
||||||
-->
|
-->
|
||||||
|
|
||||||

|

|
||||||
|
@ -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 |
|
|
||||||
| [`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 |
|
|
||||||
|
|
||||||
<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.
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { StructDefaultOptions } from './context';
|
import { StructDefaultOptions } from './options';
|
||||||
|
|
||||||
describe('StructDefaultOptions', () => {
|
describe('StructDefaultOptions', () => {
|
||||||
describe('.littleEndian', () => {
|
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.
|
* 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,
|
|
||||||
};
|
|
|
@ -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> = {};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue