refactor(struct): performance optimization

This commit is contained in:
Simon Chan 2022-05-15 13:48:25 +08:00
parent 7d5445aeae
commit 30faaa1438
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
42 changed files with 2765 additions and 2587 deletions

View file

@ -11,6 +11,7 @@
"CLSE",
"CNXN",
"Deserialization",
"DESERIALIZERS",
"Embedder",
"fluentui",
"genymobile",

File diff suppressed because it is too large Load diff

View file

@ -32,7 +32,6 @@
},
"devDependencies": {
"typescript": "4.7.1-rc",
"@types/jest": "^27.4.1",
"@yume-chan/ts-package-builder": "^1.0.0"
},
"dependencies": {

View file

@ -37,7 +37,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"jest": "^27.5.1",
"jest": "^28.1.0",
"typescript": "4.7.1-rc",
"@yume-chan/ts-package-builder": "^1.0.0"
}

View file

@ -30,7 +30,7 @@
"build:watch": "build-ts-package --incremental"
},
"devDependencies": {
"jest": "^27.5.1",
"jest": "^28.1.0",
"typescript": "4.7.1-rc",
"@yume-chan/ts-package-builder": "^1.0.0"
},

View file

@ -39,12 +39,12 @@
"web-streams-polyfill": "^4.0.0-beta.2"
},
"devDependencies": {
"@jest/globals": "^28.1.0",
"@types/node": "^17.0.17",
"@types/jest": "^27.4.1",
"@yume-chan/ts-package-builder": "^1.0.0",
"cross-env": "^7.0.3",
"jest": "^27.5.1",
"ts-jest": "^27.1.3",
"jest": "^28.1.0",
"ts-jest": "^28.0.2",
"typescript": "4.7.1-rc"
}
}

View file

@ -0,0 +1,125 @@
import { describe, expect, it } from '@jest/globals';
import { BufferedStream } from "./buffered.js";
import { ReadableStream } from "./detect.js";
function randomUint8Array(length: number) {
const array = new Uint8Array(length);
for (let i = 0; i < length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
}
class MockReadableStream extends ReadableStream<Uint8Array> {
constructor(buffers: Uint8Array[]) {
let index = 0;
super({
pull(controller) {
if (index === buffers.length) {
controller.close();
return;
}
controller.enqueue(buffers[index]!);
index += 1;
}
});
}
}
async function runTest(inputSizes: number[], readSizes: number[]) {
const totalSize = inputSizes.reduce((a, b) => a + b, 0);
const input = randomUint8Array(totalSize);
const buffers: Uint8Array[] = [];
let index = 0;
for (const size of inputSizes) {
buffers.push(input.subarray(index, index + size));
index += size;
}
const stream = new MockReadableStream(buffers);
const buffered = new BufferedStream(stream);
index = 0;
for (const size of readSizes) {
const buffer = await buffered.read(size);
expect(buffer).toEqual(input.subarray(index, index + size));
index += size;
}
}
describe('BufferedStream', () => {
describe('read 1 time', () => {
it('read 0 buffer', async () => {
const source = new MockReadableStream([]);
const buffered = new BufferedStream(source);
await expect(buffered.read(10)).rejects.toThrow();
});
it('input 1 exact buffer', async () => {
const input = randomUint8Array(10);
const source = new MockReadableStream([input]);
const buffered = new BufferedStream(source);
await expect(buffered.read(10)).resolves.toBe(input);
});
it('input 1 large buffer', () => {
return runTest([20], [10]);
});
it('read 1 small buffer', async () => {
const source = new MockReadableStream([randomUint8Array(5)]);
const buffered = new BufferedStream(source);
await expect(buffered.read(10)).rejects.toThrow();
});
it('input 2 small buffers', () => {
return runTest([5, 5], [10]);
});
it('read 2 small buffers', async () => {
const source = new MockReadableStream([randomUint8Array(5), randomUint8Array(5)]);
const buffered = new BufferedStream(source);
await expect(buffered.read(20)).rejects.toThrow();
});
it('input 2 small + large buffers', () => {
return runTest([5, 10], [10]);
});
});
describe('read 2 times', () => {
it('input 1 exact buffer', () => {
return runTest([10], [5, 5]);
});
it('input 1 large buffer', () => {
return runTest([20], [5, 5]);
});
it('input 2 exact buffers', () => {
return runTest([5, 5], [5, 5]);
});
it('input 2 exact + large buffers', () => {
return runTest([5, 10], [5, 8]);
});
it('input 2 small + large buffers', () => {
return runTest([5, 10], [7, 8]);
});
it('input 2 large buffers', () => {
return runTest([10, 10], [8, 8]);
});
it('input 3 small buffers', () => {
return runTest([3, 3, 3], [5, 4]);
});
it('input 3 small buffers 2', () => {
return runTest([3, 3, 3], [7, 2]);
});
});
});

View file

@ -13,7 +13,9 @@ export class BufferedStreamEndedError extends Error {
}
export class BufferedStream {
private buffer: Uint8Array | undefined;
private buffered: Uint8Array | undefined;
private bufferedOffset = 0;
private bufferedLength = 0;
protected readonly stream: ReadableStream<Uint8Array>;
@ -30,63 +32,74 @@ export class BufferedStream {
* @returns
*/
public async read(length: number): Promise<Uint8Array> {
let array: Uint8Array;
let result: Uint8Array;
let index: number;
if (this.buffer) {
const buffer = this.buffer;
if (buffer.byteLength > length) {
this.buffer = buffer.subarray(length);
return buffer.subarray(0, length);
if (this.buffered) {
let array = this.buffered;
const offset = this.bufferedOffset;
if (this.bufferedLength > length) {
// PERF: `subarray` is slow
// don't use it until absolutely necessary
this.bufferedOffset += length;
this.bufferedLength -= length;
return array.subarray(offset, offset + length);
}
array = new Uint8Array(length);
array.set(buffer);
index = buffer.byteLength;
this.buffer = undefined;
this.buffered = undefined;
array = array.subarray(offset);
result = new Uint8Array(length);
result.set(array);
index = this.bufferedLength;
length -= this.bufferedLength;
} else {
const { done, value } = await this.reader.read();
const { done, value: array } = await this.reader.read();
if (done) {
throw new BufferedStreamEndedError();
}
if (value.byteLength === length) {
return value;
if (array.byteLength === length) {
return array;
}
if (value.byteLength > length) {
this.buffer = value.subarray(length);
return value.subarray(0, length);
if (array.byteLength > length) {
this.buffered = array;
this.bufferedOffset = length;
this.bufferedLength = array.byteLength - length;
return array.subarray(0, length);
}
array = new Uint8Array(length);
array.set(value);
index = value.byteLength;
result = new Uint8Array(length);
result.set(array);
index = array.byteLength;
length -= array.byteLength;
}
while (index < length) {
const left = length - index;
const { done, value } = await this.reader.read();
while (length > 0) {
const { done, value: array } = await this.reader.read();
if (done) {
throw new BufferedStreamEndedError();
}
if (value.byteLength === left) {
array.set(value, index);
return array;
if (array.byteLength === length) {
result.set(array, index);
return result;
}
if (value.byteLength > left) {
array.set(value.subarray(0, left), index);
this.buffer = value.subarray(left);
return array;
if (array.byteLength > length) {
this.buffered = array;
this.bufferedOffset = length;
this.bufferedLength = array.byteLength - length;
result.set(array.subarray(0, length), index);
return result;
}
array.set(value, index);
index += value.byteLength;
result.set(array, index);
index += array.byteLength;
length -= array.byteLength;
}
return array;
return result;
}
/**
@ -95,10 +108,10 @@ export class BufferedStream {
* @returns A `ReadableStream`
*/
public release(): ReadableStream<Uint8Array> {
if (this.buffer) {
if (this.buffered) {
return new PushReadableStream<Uint8Array>(async controller => {
// Put the remaining data back to the stream
await controller.enqueue(this.buffer!);
await controller.enqueue(this.buffered!);
// Manually pipe the stream
while (true) {

View file

@ -1,10 +1,4 @@
// cspell: ignore ponyfill
import type { AbortSignal } from "web-streams-polyfill";
// TODO: Upgrade to `web-streams-polyfill@4.0.0-beta.2` once released.
// `web-streams-polyfill@4.0.0-beta.1` changed the default export to ponyfill,
// But it forgot to include `type` export so it's unusable.
// See https://github.com/MattiasBuelens/web-streams-polyfill/pull/107
export * from 'web-streams-polyfill';
/** A controller object that allows you to abort one or more DOM requests as and when desired. */

View file

@ -3,3 +3,5 @@
// Always use polyfilled version because
// Vercel doesn't support Node.js 16 (`streams/web` module) yet
export * from './detect.polyfill.js';
// export * from './detect.native.js';

View file

@ -1,10 +1,10 @@
import { ReadableStream } from "./index.js";
import { ReadableStream } from "./detect.js";
import { DuplexStreamFactory } from './transform.js';
describe('DuplexStreamFactory', () => {
it('should close all readable', async () => {
const factory = new DuplexStreamFactory();
const readable = factory.wrapReadable(new ReadableStream());
const readable = factory.wrapReadable(new ReadableStream() as any);
const reader = readable.getReader();
await factory.close();
await reader.closed;

View file

@ -1,10 +1,11 @@
{
"references": [
{
"path": "../event/tsconfig.json"
},
{
"path": "../struct/tsconfig.json"
"path": "../struct/tsconfig.build.json"
},
{
"path": "./tsconfig.test.json"

View file

@ -2,8 +2,7 @@
"extends": "./tsconfig.build.json",
"compilerOptions": {
"types": [
"node",
"jest",
"node"
],
},
"exclude": []

View file

@ -1,6 +1,6 @@
// cspell: ignore logcat
import { AdbCommandBase, BufferedStream, BufferedStreamEndedError, DecodeUtf8Stream, ReadableStream, SplitLineStream, WritableStream } from "@yume-chan/adb";
import { AdbCommandBase, AdbSubprocessNoneProtocol, BufferedStream, BufferedStreamEndedError, DecodeUtf8Stream, ReadableStream, SplitLineStream, WritableStream } from "@yume-chan/adb";
import Struct, { StructAsyncDeserializeStream } from "@yume-chan/struct";
// `adb logcat` is an alias to `adb shell logcat`
@ -63,7 +63,9 @@ export interface LogMessage extends LoggerEntry {
export async function deserializeLogMessage(stream: StructAsyncDeserializeStream): Promise<LogMessage> {
const entry = await LoggerEntry.deserialize(stream);
await stream.read(entry.headerSize - LoggerEntry.size);
if (entry.headerSize !== LoggerEntry.size) {
await stream.read(entry.headerSize - LoggerEntry.size);
}
const priority = (await stream.read(1))[0] as LogPriority;
const payload = await stream.read(entry.payloadSize - 1);
(entry as any).priority = priority;
@ -165,7 +167,10 @@ export class Logcat extends AdbCommandBase {
'-B',
...(options?.pid ? ['--pid', options.pid.toString()] : []),
...(options?.ids ? ['-b', Logcat.joinLogId(options.ids)] : [])
]);
], {
// PERF: None protocol is 25% faster then Shell protocol
protocols: [AdbSubprocessNoneProtocol],
});
bufferedStream = new BufferedStream(stdout);
},
async pull(controller) {

View file

@ -31,14 +31,14 @@
"scripts": {
"build": "build-ts-package",
"build:watch": "build-ts-package --incremental",
"test": "jest --coverage",
"//test": "jest --coverage",
"prepublishOnly": "npm run build"
},
"dependencies": {
"tslib": "^2.3.1"
},
"devDependencies": {
"jest": "^27.5.1",
"jest": "^28.1.0",
"typescript": "4.7.1-rc",
"@yume-chan/ts-package-builder": "^1.0.0"
}

View file

@ -28,7 +28,7 @@
"scripts": {
"build": "build-ts-package",
"build:watch": "build-ts-package --incremental",
"test": "jest --coverage",
"//test": "jest --coverage",
"prepublishOnly": "npm run build"
},
"dependencies": {
@ -36,7 +36,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"jest": "^27.5.1",
"jest": "^28.1.0",
"typescript": "4.7.1-rc",
"@yume-chan/ts-package-builder": "^1.0.0"
}

View file

@ -42,11 +42,11 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"@jest/globals": "^28.1.0",
"@types/dom-webcodecs": "^0.1.3",
"@types/jest": "^27.4.1",
"@yume-chan/ts-package-builder": "^1.0.0",
"gh-release-fetch": "^2.0.4",
"jest": "^27.5.1",
"jest": "^28.1.0",
"tinyh264": "^0.0.7",
"typescript": "4.7.1-rc",
"yuv-buffer": "^1.0.0",

View file

@ -1,4 +1,13 @@
module.exports = {
testMatch: ['<rootDir>/cjs/**/*.spec.js'],
testEnvironment: 'node',
/** @type {import('ts-jest').InitialOptionsTsJest} */
export default {
preset: "ts-jest/presets/default-esm",
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',
useESM: true,
},
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};

View file

@ -27,9 +27,9 @@
"main": "esm/index.js",
"types": "esm/index.d.ts",
"scripts": {
"build": "build-ts-package",
"build:watch": "build-ts-package --incremental",
"test": "jest --coverage",
"build": "tsc -b tsconfig.build.json",
"build:watch": "tsc -b tsconfig.build.json",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"prepublishOnly": "npm run build"
},
"dependencies": {
@ -37,10 +37,11 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"jest": "^27.5.1",
"typescript": "4.7.1-rc",
"@jest/globals": "^28.1.0",
"@yume-chan/ts-package-builder": "^1.0.0",
"@types/jest": "^27.4.1",
"@types/bluebird": "^3.5.36"
"cross-env": "^7.0.3",
"jest": "^28.1.0",
"ts-jest": "^28.0.2",
"typescript": "4.7.1-rc"
}
}

View file

@ -1,3 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import type { ValueOrPromise } from '../utils.js';
import { StructFieldDefinition } from './definition.js';
import type { StructFieldValue } from './field-value.js';

View file

@ -1,5 +1,3 @@
// cspell: ignore Syncbird
import type { StructAsyncDeserializeStream, StructDeserializeStream } from "./stream.js";
import type { StructFieldValue } from "./field-value.js";
import type { StructValue } from "./struct-value.js";
@ -47,7 +45,7 @@ export abstract class StructFieldDefinition<
*/
public abstract create(
options: Readonly<StructOptions>,
struct: StructValue,
structValue: StructValue,
value: TValue,
): StructFieldValue<this>;
@ -55,12 +53,12 @@ export abstract class StructFieldDefinition<
* 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.
* `SyncPromise` can be used to simplify implementation.
*/
public abstract deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream,
struct: StructValue,
structValue: StructValue,
): StructFieldValue<this>;
public abstract deserialize(
options: Readonly<StructOptions>,

View file

@ -1,3 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import type { ValueOrPromise } from "../utils.js";
import { StructFieldDefinition } from "./definition.js";
import { StructFieldValue } from "./field-value.js";

View file

@ -1,3 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { StructDefaultOptions } from './options.js';
describe('StructDefaultOptions', () => {

View file

@ -1,3 +1,5 @@
import { describe, expect, it, jest } from '@jest/globals';
import { StructValue } from "./struct-value.js";
describe('StructValue', () => {

View file

@ -1,4 +1,6 @@
import type { StructFieldValue } from "./field-value.js";
import { StructFieldValue } from "./field-value.js";
export const STRUCT_VALUE_SYMBOL = Symbol("struct-value");
/**
* A struct value is a map between keys in a struct and their field values.
@ -11,29 +13,43 @@ export class StructValue {
*/
public readonly value: Record<PropertyKey, unknown> = {};
public constructor() {
Object.defineProperty(
this.value,
STRUCT_VALUE_SYMBOL,
{ enumerable: false, value: this }
);
}
/**
* Sets a `StructFieldValue` for `key`
*
* @param key The field name
* @param value The associated `StructFieldValue`
* @param name The field name
* @param fieldValue The associated `StructFieldValue`
*/
public set(key: PropertyKey, value: StructFieldValue): void {
this.fieldValues[key] = value;
public set(name: PropertyKey, fieldValue: StructFieldValue): void {
this.fieldValues[name] = fieldValue;
Object.defineProperty(this.value, key, {
configurable: true,
enumerable: true,
get() { return value.get(); },
set(v) { value.set(v); },
});
// PERF: `Object.defineProperty` is slow
if (fieldValue.get !== StructFieldValue.prototype.get ||
fieldValue.set !== StructFieldValue.prototype.set) {
Object.defineProperty(this.value, name, {
configurable: true,
enumerable: true,
get() { return fieldValue.get(); },
set(v) { fieldValue.set(v); },
});
} else {
this.value[name] = fieldValue.get();
}
}
/**
* Gets the `StructFieldValue` for `key`
*
* @param key The field name
* @param name The field name
*/
public get(key: PropertyKey): StructFieldValue {
return this.fieldValues[key]!;
public get(name: PropertyKey): StructFieldValue {
return this.fieldValues[name]!;
}
}

View file

@ -1,3 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import Struct from './index.js';
describe('Struct', () => {

View file

@ -1,10 +1,12 @@
import { describe, expect, it, jest } from '@jest/globals';
import { StructAsyncDeserializeStream, StructDefaultOptions, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from './basic/index.js';
import { BigIntFieldDefinition, BigIntFieldType, BufferFieldSubType, FixedLengthBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, VariableLengthBufferLikeFieldDefinition } from "./index.js";
import { Struct } from './struct.js';
import { ArrayBufferFieldType, BigIntFieldDefinition, BigIntFieldType, FixedLengthArrayBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringFieldType, ArrayBufferViewFieldType, VariableLengthArrayBufferLikeFieldDefinition } from './types/index.js';
import { ValueOrPromise } from './utils.js';
import type { ValueOrPromise } from './utils.js';
class MockDeserializationStream implements StructDeserializeStream {
public buffer = new ArrayBuffer(0);
public buffer = new Uint8Array(0);
public read = jest.fn((length: number) => this.buffer);
}
@ -161,27 +163,16 @@ describe('Struct', () => {
expect(definition.type).toBe(BigIntFieldType.Uint64);
});
describe('#arrayBufferLike', () => {
describe('FixedLengthArrayBufferLikeFieldDefinition', () => {
it('`#arrayBuffer` with fixed length', () => {
describe('#uint8ArrayLike', () => {
describe('FixedLengthBufferLikeFieldDefinition', () => {
it('`#uint8Array` with fixed length', () => {
let struct = new Struct();
struct.arrayBuffer('foo', { length: 10 });
struct.uint8Array('foo', { length: 10 });
expect(struct).toHaveProperty('size', 10);
const definition = struct['_fields'][0]![1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(ArrayBufferFieldType);
expect(definition.options.length).toBe(10);
});
it('`#uint8ClampedArray` with fixed length', () => {
let struct = new Struct();
struct.uint8ClampedArray('foo', { length: 10 });
expect(struct).toHaveProperty('size', 10);
const definition = struct['_fields'][0]![1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(ArrayBufferViewFieldType);
const definition = struct['_fields'][0]![1] as FixedLengthBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.options.length).toBe(10);
});
@ -190,41 +181,27 @@ describe('Struct', () => {
struct.string('foo', { length: 10 });
expect(struct).toHaveProperty('size', 10);
const definition = struct['_fields'][0]![1] as FixedLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(StringFieldType);
const definition = struct['_fields'][0]![1] as FixedLengthBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(FixedLengthBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.options.length).toBe(10);
});
});
describe('VariableLengthArrayBufferLikeFieldDefinition', () => {
it('`#arrayBuffer` with variable length', () => {
describe('VariableLengthBufferLikeFieldDefinition', () => {
it('`#uint8Array` with variable length', () => {
const struct = new Struct().int8('barLength');
expect(struct).toHaveProperty('size', 1);
struct.arrayBuffer('bar', { lengthField: 'barLength' });
struct.uint8Array('bar', { lengthField: 'barLength' });
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][1]![1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(ArrayBufferFieldType);
const definition = struct['_fields'][1]![1] as VariableLengthBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.options.lengthField).toBe('barLength');
});
it('`#uint8ClampedArray` with variable length', () => {
const struct = new Struct().int8('barLength');
expect(struct).toHaveProperty('size', 1);
struct.uint8ClampedArray('bar', { lengthField: 'barLength' });
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][1]![1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(ArrayBufferViewFieldType);
expect(definition.options.lengthField).toBe('barLength');
});
it('`#string` with variable length', () => {
const struct = new Struct().int8('barLength');
expect(struct).toHaveProperty('size', 1);
@ -232,9 +209,9 @@ describe('Struct', () => {
struct.string('bar', { lengthField: 'barLength' });
expect(struct).toHaveProperty('size', 1);
const definition = struct['_fields'][1]![1] as VariableLengthArrayBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthArrayBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(StringFieldType);
const definition = struct['_fields'][1]![1] as VariableLengthBufferLikeFieldDefinition;
expect(definition).toBeInstanceOf(VariableLengthBufferLikeFieldDefinition);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.options.lengthField).toBe('barLength');
});
});
@ -277,8 +254,8 @@ describe('Struct', () => {
const stream = new MockDeserializationStream();
stream.read
.mockReturnValueOnce(new Uint8Array([2]).buffer)
.mockReturnValueOnce(new Uint8Array([0, 16]).buffer);
.mockReturnValueOnce(new Uint8Array([2]))
.mockReturnValueOnce(new Uint8Array([0, 16]));
const result = await struct.deserialize(stream);
expect(result).toEqual({ foo: 2, bar: 16 });
@ -291,15 +268,15 @@ describe('Struct', () => {
it('should deserialize with dynamic size fields', async () => {
const struct = new Struct()
.int8('fooLength')
.uint8ClampedArray('foo', { lengthField: 'fooLength' });
.uint8Array('foo', { lengthField: 'fooLength' });
const stream = new MockDeserializationStream();
stream.read
.mockReturnValueOnce(new Uint8Array([2]).buffer)
.mockReturnValueOnce(new Uint8Array([3, 4]).buffer);
.mockReturnValueOnce(new Uint8Array([2]))
.mockReturnValueOnce(new Uint8Array([3, 4]));
const result = await struct.deserialize(stream);
expect(result).toEqual({ fooLength: 2, foo: new Uint8ClampedArray([3, 4]) });
expect(result).toEqual({ fooLength: 2, foo: new Uint8Array([3, 4]) });
expect(stream.read).toBeCalledTimes(2);
expect(stream.read).nthCalledWith(1, 1);
expect(stream.read).nthCalledWith(2, 2);
@ -339,40 +316,40 @@ describe('Struct', () => {
});
describe('#postDeserialize', () => {
it('can throw errors', async () => {
it('can throw errors', () => {
const struct = new Struct();
const callback = jest.fn(() => { throw new Error('mock'); });
struct.postDeserialize(callback);
const stream = new MockDeserializationStream();
expect(struct.deserialize(stream)).rejects.toThrowError('mock');
expect(() => struct.deserialize(stream)).toThrowError('mock');
expect(callback).toBeCalledTimes(1);
});
it('can replace return value', async () => {
it('can replace return value', () => {
const struct = new Struct();
const callback = jest.fn(() => 'mock');
struct.postDeserialize(callback);
const stream = new MockDeserializationStream();
expect(struct.deserialize(stream)).resolves.toBe('mock');
expect(struct.deserialize(stream)).toBe('mock');
expect(callback).toBeCalledTimes(1);
expect(callback).toBeCalledWith({});
});
it('can return nothing', async () => {
it('can return nothing', () => {
const struct = new Struct();
const callback = jest.fn();
struct.postDeserialize(callback);
const stream = new MockDeserializationStream();
const result = await struct.deserialize(stream);
const result = struct.deserialize(stream);
expect(callback).toBeCalledTimes(1);
expect(callback).toBeCalledWith(result);
});
it('should overwrite callback', async () => {
it('should overwrite callback', () => {
const struct = new Struct();
const callback1 = jest.fn();
@ -382,7 +359,7 @@ describe('Struct', () => {
struct.postDeserialize(callback2);
const stream = new MockDeserializationStream();
await struct.deserialize(stream);
struct.deserialize(stream);
expect(callback1).toBeCalledTimes(0);
expect(callback2).toBeCalledTimes(1);
@ -404,9 +381,9 @@ describe('Struct', () => {
it('should serialize with dynamic size fields', () => {
const struct = new Struct()
.int8('fooLength')
.arrayBuffer('foo', { lengthField: 'fooLength' });
.uint8Array('foo', { lengthField: 'fooLength' });
const result = new Uint8Array(struct.serialize({ foo: new Uint8Array([0x03, 0x04, 0x05]).buffer }));
const result = new Uint8Array(struct.serialize({ foo: new Uint8Array([0x03, 0x04, 0x05]) }));
expect(result).toEqual(new Uint8Array([0x03, 0x03, 0x04, 0x05]));
});

View file

@ -1,6 +1,4 @@
// cspell: ignore Syncbird
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions } from './basic/index.js';
import { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, STRUCT_VALUE_SYMBOL } from './basic/index.js';
import { StructDefaultOptions, StructValue } from './basic/index.js';
import { SyncPromise } from "./sync-promise.js";
import { BigIntFieldDefinition, BigIntFieldType, BufferFieldSubType, FixedLengthBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringBufferFieldSubType, Uint8ArrayBufferFieldSubType, VariableLengthBufferLikeFieldDefinition, type FixedLengthBufferLikeFieldOptions, type LengthField, type VariableLengthBufferLikeFieldOptions } from './types/index.js';
@ -542,21 +540,25 @@ export class Struct<
public deserialize(
stream: StructDeserializeStream | StructAsyncDeserializeStream,
): ValueOrPromise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> {
const value = new StructValue();
Object.defineProperties(value.value, this._extra);
const structValue = new StructValue();
Object.defineProperties(structValue.value, this._extra);
return SyncPromise
.each(this._fields, ([name, definition]) => {
return SyncPromise
.try(() => {
return definition.deserialize(this.options, stream as any, value);
})
.then(fieldValue => {
value.set(name, fieldValue);
});
.try(() => {
let result = SyncPromise.resolve();
for (const [name, definition] of this._fields) {
result = result
.then(() =>
definition.deserialize(this.options, stream as any, structValue)
)
.then(fieldValue => {
structValue.set(name, fieldValue);
});
}
return result;
})
.then(() => {
const object = value.value;
const object = structValue.value;
// Run `postDeserialized`
if (this._postDeserialized) {
@ -576,18 +578,32 @@ export class Struct<
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output: Uint8Array): number;
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, output?: Uint8Array): Uint8Array | number {
const value = new StructValue();
for (const [name, definition] of this._fields) {
const fieldValue = definition.create(this.options, value, (init as any)[name]);
value.set(name, fieldValue);
let structValue: StructValue;
if (STRUCT_VALUE_SYMBOL in init) {
structValue = (init as any)[STRUCT_VALUE_SYMBOL];
for (const [key, value] of Object.entries(init)) {
const fieldValue = structValue.get(key);
if (fieldValue) {
fieldValue.set(value);
}
}
} else {
structValue = new StructValue();
for (const [name, definition] of this._fields) {
const fieldValue = definition.create(
this.options,
structValue,
(init as any)[name]
);
structValue.set(name, fieldValue);
}
}
let structSize = 0;
const fieldsInfo: { fieldValue: StructFieldValue, size: number; }[] = [];
for (const [name] of this._fields) {
const fieldValue = value.get(name);
const fieldValue = structValue.get(name);
const size = fieldValue.getSize();
fieldsInfo.push({ fieldValue, size });
structSize += size;

View file

@ -0,0 +1,189 @@
import { describe, expect, it, jest, test } from "@jest/globals";
import { SyncPromise } from "./sync-promise.js";
describe('SyncPromise', () => {
describe('constructor', () => {
it('should call executor', () => {
const executor = jest.fn((resolve) => {
setTimeout(() => {
resolve(42);
}, 10);
});
new SyncPromise(executor);
expect(executor).toHaveBeenCalledTimes(1);
});
it('should asynchronously resolve', async () => {
const promise = new SyncPromise((resolve) => {
setTimeout(() => {
resolve(42);
}, 10);
});
await expect(promise).resolves.toBe(42);
await expect(promise.then()).resolves.toBe(42);
await expect(promise.valueOrPromise()).resolves.toBe(42);
});
it('should asynchronously reject', async () => {
const promise = new SyncPromise((_, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, 10);
});
await expect(promise).rejects.toThrow('error');
await expect(promise.then()).rejects.toThrow('error');
await expect(promise.valueOrPromise()).rejects.toThrow('error');
});
it('should synchronously resolve with value', async () => {
const promise = new SyncPromise((resolve) => {
resolve(42);
});
await expect(promise).resolves.toBe(42);
await expect(promise.then()).resolves.toBe(42);
expect(promise.valueOrPromise()).toBe(42);
});
it('should synchronously resolve with promise', async () => {
const promise = new SyncPromise((resolve) => {
resolve(
new Promise(
resolve =>
setTimeout(
() => resolve(42),
10
)
)
);
});
await expect(promise).resolves.toBe(42);
await expect(promise.then()).resolves.toBe(42);
await expect(promise.valueOrPromise()).resolves.toBe(42);
});
it('should synchronously resolve with resolved SyncPromise', async () => {
const promise = new SyncPromise((resolve) => {
resolve(
new SyncPromise(
resolve =>
resolve(42),
)
);
});
await expect(promise).resolves.toBe(42);
await expect(promise.then()).resolves.toBe(42);
expect(promise.valueOrPromise()).toBe(42);
});
it('should synchronously resolve with rejected SyncPromise', async () => {
const promise = new SyncPromise((resolve) => {
resolve(
new SyncPromise(
(_, reject) =>
reject(new Error('error'))
)
);
});
await expect(promise).rejects.toThrowError('error');
await expect(promise.then()).rejects.toThrowError('error');
expect(() => promise.valueOrPromise()).toThrowError('error');
});
it('should synchronously resolve with unsettled SyncPromise', async () => {
const promise = new SyncPromise((resolve) => {
resolve(
new SyncPromise(
resolve =>
setTimeout(
() => resolve(42),
10
)
)
);
});
await expect(promise).resolves.toBe(42);
await expect(promise.then()).resolves.toBe(42);
await expect(promise.valueOrPromise()).resolves.toBe(42);
});
it('should synchronously reject with error', async () => {
const promise = new SyncPromise((_, reject) => {
reject(new Error('error'));
});
await expect(promise).rejects.toThrow('error');
await expect(promise.then()).rejects.toThrow('error');
expect(() => promise.valueOrPromise()).toThrow('error');
});
it('should catch synchronous error', async () => {
const promise = new SyncPromise(() => {
throw new Error('error');
});
await expect(promise).rejects.toThrow('error');
await expect(promise.then()).rejects.toThrow('error');
expect(() => promise.valueOrPromise()).toThrow('error');
});
describe('should ignore multiple result', () => {
test('multiple resolves', async () => {
const promise = new SyncPromise((resolve) => {
resolve(42);
resolve(43);
});
await expect(promise).resolves.toBe(42);
});
test('multiple rejects', async () => {
const promise = new SyncPromise((_, reject) => {
reject(new Error('error'));
reject(new Error('error2'));
});
await expect(promise).rejects.toThrow('error');
});
test('mixed', async () => {
const promise = new SyncPromise((resolve, reject) => {
resolve(42);
reject(new Error('error2'));
});
await expect(promise).resolves.toBe(42);
});
test('mixed with throw', async () => {
const promise = new SyncPromise((resolve) => {
resolve(42);
throw new Error('error2');
});
await expect(promise).resolves.toBe(42);
});
});
});
describe('#then', () => {
it('chain a sync value', async () => {
let promise = new SyncPromise(resolve => resolve(42));
promise = promise.then(() => 'foo');
await expect(promise).resolves.toBe('foo');
await expect(promise.then()).resolves.toBe('foo');
expect(promise.valueOrPromise()).toBe('foo');
});
it('chain a async value', async () => {
let promise = new SyncPromise(resolve => resolve(42));
promise = promise.then(
() =>
new Promise(
(resolve) =>
setTimeout(
() => resolve('foo'),
10
)
)
);
await expect(promise).resolves.toBe('foo');
await expect(promise.then()).resolves.toBe('foo');
expect(promise.valueOrPromise()).resolves.toBe('foo');
});
});
});

View file

@ -1,48 +1,136 @@
export class SyncPromise<T> extends Promise<T> {
private resolved: boolean;
private rejected: boolean;
private result: unknown;
// PERF: Once a promise becomes async, it can't go back to sync again,
// so bypass all checks in `SyncPromise`
class AsyncPromise<T> extends Promise<T> {
public valueOrPromise(): T | PromiseLike<T> {
return this;
}
}
enum State {
Pending,
Fulfilled,
Rejected,
}
function fulfilledThen<T, TResult1 = T>(
this: SyncPromise<T>,
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
): SyncPromise<TResult1> {
if (onfulfilled) {
return SyncPromise.try(() => onfulfilled(this.result as T));
}
return this as unknown as SyncPromise<TResult1>;
}
function fulfilledValue<T>(
this: SyncPromise<T>,
) {
return this.result as T;
}
function rejectedThen<T, TResult1 = T, TResult2 = never>(
this: SyncPromise<T>,
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
) {
if (onrejected) {
return SyncPromise.try(() => onrejected(this.result));
}
return this as unknown as SyncPromise<TResult1 | TResult2>;
}
function rejectedValue<T>(
this: SyncPromise<T>,
): never {
throw this.result;
}
interface SyncPromiseThen<T> {
<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
): SyncPromise<TResult1 | TResult2>;
}
export class SyncPromise<T> extends AsyncPromise<T> {
// Because `super.then` will only be called when `this` is asynchronously fulfilled,
// let it create `AsyncPromise` instead, so asynchronous path won't check for sync state anymore.
public static [Symbol.species] = AsyncPromise;
public static override reject<T = never>(reason?: any): SyncPromise<T> {
return new SyncPromise<T>(
(resolve, reject) => {
reject(reason);
}
);
}
public static override resolve(): SyncPromise<void>;
public static override resolve<T>(value: T | PromiseLike<T>): SyncPromise<T>;
public static override resolve<T>(value?: T | PromiseLike<T>): SyncPromise<T> {
return new SyncPromise((resolve, reject) => {
resolve(value!);
});
}
// `Promise.resolve` asynchronously calls `resolve`
// So we need to write our own.
if (value instanceof SyncPromise) {
return value;
}
public static each<T>(array: T[], callback: (item: T, index: number) => SyncPromise<void>): SyncPromise<void> {
return array.reduce((prev, item, index) => {
return prev.then(() => {
return callback(item, index);
});
}, SyncPromise.resolve());
return new SyncPromise<T>(
(resolve) => {
resolve(value!);
}
);
}
public static try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T> {
return new SyncPromise((resolve, reject) => {
try {
resolve(executor());
} catch (e) {
reject(e);
}
});
try {
return SyncPromise.resolve(executor());
} catch (e) {
return SyncPromise.reject(e);
}
}
public state: State;
public result: unknown;
public constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
let sync = true;
let settled = false;
let resolved = false;
let rejected = false;
let state: State = State.Pending;
let result: unknown = undefined;
let handleThen: SyncPromise<T>['then'] = Promise.prototype.then as any;
let handleValueOrPromise: () => T | PromiseLike<T> = () => this;
const handleResolveCore = (value: T) => {
state = State.Fulfilled;
result = value;
handleThen = fulfilledThen;
handleValueOrPromise = fulfilledValue;
};
const handleRejectCore = (reason?: any) => {
state = State.Rejected;
result = reason;
handleThen = rejectedThen;
handleValueOrPromise = rejectedValue;
};
let settled = false;
let sync = true;
super((resolve, reject) => {
try {
executor((value) => {
if (settled) {
return;
}
const handleReject = (reason?: any) => {
if (settled) { return; }
settled = true;
if (!sync) {
reject(reason);
return;
}
handleRejectCore(reason);
};
try {
executor((value: T | PromiseLike<T>) => {
if (settled) { return; }
settled = true;
if (!sync) {
@ -50,115 +138,41 @@ export class SyncPromise<T> extends Promise<T> {
return;
}
if (typeof value === 'object' &&
value !== null &&
'then' in value &&
typeof value.then === 'function'
) {
if (typeof value === 'object' && value !== null && typeof (value as any).then === 'function') {
if (value instanceof SyncPromise) {
if (value.resolved) {
resolved = true;
result = value.result;
return;
} else if (value.rejected) {
rejected = true;
result = value.result;
return;
switch (value.state) {
case State.Fulfilled:
handleResolveCore(value.result as T);
return;
case State.Rejected:
handleRejectCore(value.result);
return;
}
}
resolve(value);
return;
} else {
handleResolveCore(value as T);
}
resolved = true;
result = value;
}, (reason) => {
if (settled) {
return;
}
settled = true;
if (!sync) {
reject(reason);
return;
}
rejected = true;
result = reason;
});
}, handleReject);
} catch (e) {
if (settled) {
return;
}
settled = true;
if (!sync) {
reject(e);
return;
}
rejected = true;
result = e;
handleReject(e);
}
});
sync = false;
this.resolved = resolved;
this.rejected = rejected;
this.state = state;
this.result = result;
this.then = handleThen;
this.valueOrPromise = handleValueOrPromise;
sync = false;
}
public override then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
): SyncPromise<TResult1 | TResult2> {
if (this.resolved) {
if (onfulfilled) {
return new SyncPromise((resolve, reject) => {
try {
resolve(onfulfilled(this.result as T));
} catch (e) {
reject(e);
}
});
}
return this as unknown as SyncPromise<TResult1 | TResult2>;
}
if (this.rejected) {
if (onrejected) {
return new SyncPromise((resolve, reject) => {
try {
resolve(onrejected(this.result));
} catch (e) {
reject(e);
}
});
}
return this as unknown as SyncPromise<TResult1 | TResult2>;
}
return super.then(onfulfilled, onrejected) as unknown as SyncPromise<TResult1 | TResult2>;
}
public override then = Promise.prototype.then as SyncPromiseThen<T>;
public override catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
): Promise<T | TResult> {
): SyncPromise<T | TResult> {
return this.then(undefined, onrejected);
}
public valueOrPromise(): T | PromiseLike<T> {
if (this.resolved) {
return this.result as T;
}
if (this.rejected) {
throw this.result;
}
return this as Promise<T>;
}
}

View file

@ -1,5 +1,3 @@
// cspell: ignore syncbird
import { getBigInt64, getBigUint64, setBigInt64, setBigUint64 } from '@yume-chan/dataview-bigint-polyfill/esm/fallback.js';
import { StructFieldDefinition, StructFieldValue, StructValue, type StructAsyncDeserializeStream, type StructDeserializeStream, type StructOptions } from "../basic/index.js";
import { SyncPromise } from "../sync-promise.js";

View file

@ -1,3 +1,5 @@
import { describe, expect, it, jest } from '@jest/globals';
import { StructDefaultOptions, StructDeserializeStream, StructValue } from '../../basic/index.js';
import { BufferFieldSubType, BufferLikeFieldDefinition, EMPTY_UINT8_ARRAY, StringBufferFieldSubType, Uint8ArrayBufferFieldSubType } from './base.js';
@ -25,8 +27,8 @@ describe('Types', () => {
});
it('`#getSize` should return the `byteLength` of the `Uint8Array`', () => {
const arrayBuffer = new Uint8Array(10);
expect(Uint8ArrayBufferFieldSubType.Instance.getSize(arrayBuffer)).toBe(10);
const array = new Uint8Array(10);
expect(Uint8ArrayBufferFieldSubType.Instance.getSize(array)).toBe(10);
});
});
@ -72,7 +74,7 @@ describe('Types', () => {
const fieldValue = await definition.deserialize(StructDefaultOptions, context, struct);
expect(context.read).toBeCalledTimes(1);
expect(context.read).toBeCalledWith(size);
expect(fieldValue).toHaveProperty('arrayBuffer', array);
expect(fieldValue).toHaveProperty('array', array);
expect(fieldValue.get()).toBe(array);
});
@ -99,7 +101,7 @@ describe('Types', () => {
describe('ArrayBufferLikeFieldValue', () => {
describe('#set', () => {
it('should clear `arrayBuffer` field', async () => {
it('should clear `array` field', async () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(Uint8ArrayBufferFieldSubType.Instance, size);
@ -113,12 +115,12 @@ describe('Types', () => {
const newValue = new Uint8Array(20);
fieldValue.set(newValue);
expect(fieldValue.get()).toBe(newValue);
expect(fieldValue).toHaveProperty('arrayBuffer', undefined);
expect(fieldValue).toHaveProperty('array', undefined);
});
});
describe('#serialize', () => {
it('should be able to serialize with cached `arrayBuffer`', async () => {
it('should be able to serialize with cached `array`', async () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(Uint8ArrayBufferFieldSubType.Instance, size);

View file

@ -1,5 +1,3 @@
// cspell: ignore syncbird
import { StructFieldDefinition, StructFieldValue, StructValue, type StructAsyncDeserializeStream, type StructDeserializeStream, type StructOptions } from '../../basic/index.js';
import { SyncPromise } from "../../sync-promise.js";
import { decodeUtf8, encodeUtf8, type ValueOrPromise } from "../../utils.js";

View file

@ -1,3 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { Uint8ArrayBufferFieldSubType } from "./base.js";
import { FixedLengthBufferLikeFieldDefinition } from "./fixed-length.js";

View file

@ -1,8 +1,10 @@
import { describe, expect, it, jest } from '@jest/globals';
import { StructDefaultOptions, StructFieldValue, StructValue } from "../../basic/index.js";
import { BufferFieldSubType, EMPTY_UINT8_ARRAY, Uint8ArrayBufferFieldSubType } from "./base.js";
import { VariableLengthBufferLikeFieldDefinition, VariableLengthBufferLikeFieldLengthValue, VariableLengthBufferLikeStructFieldValue } from "./variable-length.js";
class MockOriginalFieldValue extends StructFieldValue {
class MockLengthFieldValue extends StructFieldValue {
public constructor() {
super({} as any, {} as any, {} as any, {});
}
@ -21,8 +23,8 @@ class MockOriginalFieldValue extends StructFieldValue {
}
describe("Types", () => {
describe("VariableLengthArrayBufferLikeFieldLengthValue", () => {
class MockArrayBufferFieldValue extends StructFieldValue {
describe("VariableLengthBufferLikeFieldLengthValue", () => {
class MockBufferLikeFieldValue extends StructFieldValue {
public constructor() {
super({ options: {} } as any, {} as any, {} as any, {});
}
@ -38,8 +40,8 @@ describe("Types", () => {
describe("#getSize", () => {
it("should return size of its original field value", () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -58,8 +60,8 @@ describe("Types", () => {
describe("#get", () => {
it("should return size of its `arrayBufferField`", async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -80,8 +82,8 @@ describe("Types", () => {
});
it("should return size of its `arrayBufferField` as string", async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -104,8 +106,8 @@ describe("Types", () => {
describe("#set", () => {
it("should does nothing", async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -122,8 +124,8 @@ describe("Types", () => {
describe("#serialize", () => {
it("should call `serialize` of its `originalField`", async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -153,8 +155,8 @@ describe("Types", () => {
});
it("should stringify its length if `originalField` is a string", async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -184,8 +186,8 @@ describe("Types", () => {
});
it("should stringify its length in specified radix if `originalField` is a string", async () => {
const mockOriginalFieldValue = new MockOriginalFieldValue();
const mockArrayBufferFieldValue = new MockArrayBufferFieldValue();
const mockOriginalFieldValue = new MockLengthFieldValue();
const mockArrayBufferFieldValue = new MockBufferLikeFieldValue();
const lengthFieldValue = new VariableLengthBufferLikeFieldLengthValue(
mockOriginalFieldValue,
mockArrayBufferFieldValue,
@ -225,7 +227,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition(
@ -235,26 +237,26 @@ describe("Types", () => {
const value = EMPTY_UINT8_ARRAY;
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
value,
);
expect(arrayBufferFieldValue).toHaveProperty("definition", arrayBufferFieldDefinition);
expect(arrayBufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty("struct", struct);
expect(arrayBufferFieldValue).toHaveProperty("value", value);
expect(arrayBufferFieldValue).toHaveProperty("arrayBuffer", undefined);
expect(arrayBufferFieldValue).toHaveProperty("length", undefined);
expect(bufferFieldValue).toHaveProperty("definition", arrayBufferFieldDefinition);
expect(bufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(bufferFieldValue).toHaveProperty("struct", struct);
expect(bufferFieldValue).toHaveProperty("value", value);
expect(bufferFieldValue).toHaveProperty("array", undefined);
expect(bufferFieldValue).toHaveProperty("length", undefined);
});
it("should forward parameters with `arrayBuffer`", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition(
@ -264,7 +266,7 @@ describe("Types", () => {
const value = new Uint8Array(100);
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
@ -272,19 +274,19 @@ describe("Types", () => {
value,
);
expect(arrayBufferFieldValue).toHaveProperty("definition", arrayBufferFieldDefinition);
expect(arrayBufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty("struct", struct);
expect(arrayBufferFieldValue).toHaveProperty("value", value);
expect(arrayBufferFieldValue).toHaveProperty("array", value);
expect(arrayBufferFieldValue).toHaveProperty("length", 100);
expect(bufferFieldValue).toHaveProperty("definition", arrayBufferFieldDefinition);
expect(bufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(bufferFieldValue).toHaveProperty("struct", struct);
expect(bufferFieldValue).toHaveProperty("value", value);
expect(bufferFieldValue).toHaveProperty("array", value);
expect(bufferFieldValue).toHaveProperty("length", 100);
});
it("should replace `lengthField` on `struct`", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition(
@ -294,15 +296,15 @@ describe("Types", () => {
const value = EMPTY_UINT8_ARRAY;
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
value,
);
expect(arrayBufferFieldValue["lengthFieldValue"]).toBeInstanceOf(StructFieldValue);
expect(struct.fieldValues[lengthField]).toBe(arrayBufferFieldValue["lengthFieldValue"]);
expect(bufferFieldValue["lengthFieldValue"]).toBeInstanceOf(StructFieldValue);
expect(struct.fieldValues[lengthField]).toBe(bufferFieldValue["lengthFieldValue"]);
});
});
@ -327,7 +329,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldType = new MockArrayBufferFieldType();
@ -338,7 +340,7 @@ describe("Types", () => {
const value = new Uint8Array(100);
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
@ -346,7 +348,7 @@ describe("Types", () => {
value,
);
expect(arrayBufferFieldValue.getSize()).toBe(100);
expect(bufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.toValue).toBeCalledTimes(0);
expect(arrayBufferFieldType.toBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.getSize).toBeCalledTimes(0);
@ -356,7 +358,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldType = new MockArrayBufferFieldType();
@ -367,7 +369,7 @@ describe("Types", () => {
const value = new Uint8Array(100);
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
@ -375,19 +377,19 @@ describe("Types", () => {
);
arrayBufferFieldType.size = 100;
expect(arrayBufferFieldValue.getSize()).toBe(100);
expect(bufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.toValue).toBeCalledTimes(0);
expect(arrayBufferFieldType.toBuffer).toBeCalledTimes(0);
expect(arrayBufferFieldType.getSize).toBeCalledTimes(1);
expect(arrayBufferFieldValue).toHaveProperty("arrayBuffer", undefined);
expect(arrayBufferFieldValue).toHaveProperty("length", 100);
expect(bufferFieldValue).toHaveProperty("array", undefined);
expect(bufferFieldValue).toHaveProperty("length", 100);
});
it("should call `toArrayBuffer` of its `type` if it does not support `getSize`", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldType = new MockArrayBufferFieldType();
@ -398,7 +400,7 @@ describe("Types", () => {
const value = new Uint8Array(100);
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
@ -406,12 +408,12 @@ describe("Types", () => {
);
arrayBufferFieldType.size = -1;
expect(arrayBufferFieldValue.getSize()).toBe(100);
expect(bufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.toValue).toBeCalledTimes(0);
expect(arrayBufferFieldType.toBuffer).toBeCalledTimes(1);
expect(arrayBufferFieldType.getSize).toBeCalledTimes(1);
expect(arrayBufferFieldValue).toHaveProperty("arrayBuffer", value);
expect(arrayBufferFieldValue).toHaveProperty("length", 100);
expect(bufferFieldValue).toHaveProperty("array", value);
expect(bufferFieldValue).toHaveProperty("length", 100);
});
});
@ -420,7 +422,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition(
@ -430,7 +432,7 @@ describe("Types", () => {
const value = new Uint8Array(100);
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
@ -439,16 +441,16 @@ describe("Types", () => {
);
const newValue = new ArrayBuffer(100);
arrayBufferFieldValue.set(newValue);
expect(arrayBufferFieldValue.get()).toBe(newValue);
expect(arrayBufferFieldValue).toHaveProperty("arrayBuffer", undefined);
bufferFieldValue.set(newValue);
expect(bufferFieldValue.get()).toBe(newValue);
expect(bufferFieldValue).toHaveProperty("array", undefined);
});
it("should clear length", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition(
@ -458,7 +460,7 @@ describe("Types", () => {
const value = new Uint8Array(100);
const arrayBufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
const bufferFieldValue = new VariableLengthBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
struct,
@ -467,8 +469,8 @@ describe("Types", () => {
);
const newValue = new ArrayBuffer(100);
arrayBufferFieldValue.set(newValue);
expect(arrayBufferFieldValue).toHaveProperty("length", undefined);
bufferFieldValue.set(newValue);
expect(bufferFieldValue).toHaveProperty("length", undefined);
});
});
});
@ -489,7 +491,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
@ -511,7 +513,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
@ -533,7 +535,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const radix = 8;
@ -558,7 +560,7 @@ describe("Types", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
@ -567,25 +569,25 @@ describe("Types", () => {
);
const value = new Uint8Array(100);
const arrayBufferFieldValue = definition.create(
const bufferFieldValue = definition.create(
StructDefaultOptions,
struct,
value,
);
expect(arrayBufferFieldValue).toHaveProperty("definition", definition);
expect(arrayBufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty("struct", struct);
expect(arrayBufferFieldValue).toHaveProperty("value", value);
expect(arrayBufferFieldValue).toHaveProperty("arrayBuffer", undefined);
expect(arrayBufferFieldValue).toHaveProperty("length", undefined);
expect(bufferFieldValue).toHaveProperty("definition", definition);
expect(bufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(bufferFieldValue).toHaveProperty("struct", struct);
expect(bufferFieldValue).toHaveProperty("value", value);
expect(bufferFieldValue).toHaveProperty("array", undefined);
expect(bufferFieldValue).toHaveProperty("length", undefined);
});
it("should create a `VariableLengthArrayBufferLikeFieldValue` with `arrayBuffer`", () => {
const struct = new StructValue();
const lengthField = "foo";
const originalLengthFieldValue = new MockOriginalFieldValue();
const originalLengthFieldValue = new MockLengthFieldValue();
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
@ -594,19 +596,19 @@ describe("Types", () => {
);
const value = new Uint8Array(100);
const arrayBufferFieldValue = definition.create(
const bufferFieldValue = definition.create(
StructDefaultOptions,
struct,
value,
value,
);
expect(arrayBufferFieldValue).toHaveProperty("definition", definition);
expect(arrayBufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty("struct", struct);
expect(arrayBufferFieldValue).toHaveProperty("value", value);
expect(arrayBufferFieldValue).toHaveProperty("arrayBuffer", value);
expect(arrayBufferFieldValue).toHaveProperty("length", 100);
expect(bufferFieldValue).toHaveProperty("definition", definition);
expect(bufferFieldValue).toHaveProperty("options", StructDefaultOptions);
expect(bufferFieldValue).toHaveProperty("struct", struct);
expect(bufferFieldValue).toHaveProperty("value", value);
expect(bufferFieldValue).toHaveProperty("array", value);
expect(bufferFieldValue).toHaveProperty("length", 100);
});
});
});

View file

@ -1,50 +1,137 @@
import { describe, expect, it, jest, test } from '@jest/globals';
import { StructDefaultOptions, StructDeserializeStream, StructValue } from "../basic/index.js";
import { NumberFieldDefinition, NumberFieldType } from "./number.js";
function testEndian(type: NumberFieldType, min: number, max: number, littleEndian: boolean) {
test('min', () => {
const buffer = new ArrayBuffer(type.size);
const view = new DataView(buffer);
(view[`set${type.signed ? 'I' : 'Ui'}nt${type.size * 8}` as keyof DataView] as any)(0, min, littleEndian);
let output = type.deserializer(new Uint8Array(buffer), littleEndian);
output = type.convertSign(output);
expect(output).toBe(min);
});
test('1', () => {
const buffer = new ArrayBuffer(type.size);
const view = new DataView(buffer);
const input = 1;
(view[`set${type.signed ? 'I' : 'Ui'}nt${type.size * 8}` as keyof DataView] as any)(0, input, littleEndian);
let output = type.deserializer(new Uint8Array(buffer), littleEndian);
output = type.convertSign(output);
expect(output).toBe(input);
});
test('max', () => {
const buffer = new ArrayBuffer(type.size);
const view = new DataView(buffer);
(view[`set${type.signed ? 'I' : 'Ui'}nt${type.size * 8}` as keyof DataView] as any)(0, max, littleEndian);
let output = type.deserializer(new Uint8Array(buffer), littleEndian);
output = type.convertSign(output);
expect(output).toBe(max);
});
}
function testDeserialize(type: NumberFieldType) {
if (type.size === 1) {
if (type.signed) {
testEndian(type, 2 ** (type.size * 8) / -2, 2 ** (type.size * 8) / 2 - 1, false);
} else {
testEndian(type, 0, 2 ** (type.size * 8) - 1, false);
}
} else {
if (type.signed) {
describe('big endian', () => {
testEndian(type, 2 ** (type.size * 8) / -2, 2 ** (type.size * 8) / 2 - 1, false);
});
describe('little endian', () => {
testEndian(type, 2 ** (type.size * 8) / -2, 2 ** (type.size * 8) / 2 - 1, true);
});
} else {
describe('big endian', () => {
testEndian(type, 0, 2 ** (type.size * 8) - 1, false);
});
describe('little endian', () => {
testEndian(type, 0, 2 ** (type.size * 8) - 1, true);
});
}
}
}
describe("Types", () => {
describe("Number", () => {
describe("NumberFieldType", () => {
it("Int8 validation", () => {
describe('Int8', () => {
const key = "Int8";
expect(NumberFieldType[key]).toHaveProperty("size", 1);
expect(NumberFieldType[key]).toHaveProperty("dataViewGetter", "get" + key);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
test('basic', () => {
expect(NumberFieldType[key]).toHaveProperty("size", 1);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
});
testDeserialize(NumberFieldType[key]);
});
it("Uint8 validation", () => {
describe('Uint8', () => {
const key = "Uint8";
expect(NumberFieldType[key]).toHaveProperty("size", 1);
expect(NumberFieldType[key]).toHaveProperty("dataViewGetter", "get" + key);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
test('basic', () => {
expect(NumberFieldType[key]).toHaveProperty("size", 1);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
});
testDeserialize(NumberFieldType[key]);
});
it("Int16 validation", () => {
describe('Int16', () => {
const key = "Int16";
expect(NumberFieldType[key]).toHaveProperty("size", 2);
expect(NumberFieldType[key]).toHaveProperty("dataViewGetter", "get" + key);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
test('basic', () => {
expect(NumberFieldType[key]).toHaveProperty("size", 2);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
});
testDeserialize(NumberFieldType[key]);
});
it("Uint16 validation", () => {
describe('Uint16', () => {
const key = "Uint16";
expect(NumberFieldType[key]).toHaveProperty("size", 2);
expect(NumberFieldType[key]).toHaveProperty("dataViewGetter", "get" + key);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
test('basic', () => {
expect(NumberFieldType[key]).toHaveProperty("size", 2);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
});
testDeserialize(NumberFieldType[key]);
});
it("Int32 validation", () => {
describe('Int32', () => {
const key = "Int32";
expect(NumberFieldType[key]).toHaveProperty("size", 4);
expect(NumberFieldType[key]).toHaveProperty("dataViewGetter", "get" + key);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
test('basic', () => {
expect(NumberFieldType[key]).toHaveProperty("size", 4);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
});
testDeserialize(NumberFieldType[key]);
});
it("Uint32 validation", () => {
describe('Uint32', () => {
const key = "Uint32";
expect(NumberFieldType[key]).toHaveProperty("size", 4);
expect(NumberFieldType[key]).toHaveProperty("dataViewGetter", "get" + key);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
test('basic', () => {
expect(NumberFieldType[key]).toHaveProperty("size", 4);
expect(NumberFieldType[key]).toHaveProperty("dataViewSetter", "set" + key);
});
testDeserialize(NumberFieldType[key]);
});
});
describe("NumberFieldDefinition", () => {
@ -60,13 +147,13 @@ describe("Types", () => {
});
describe("#deserialize", () => {
it("should deserialize Uint8", async () => {
it("should deserialize Uint8", () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]));
const stream: StructDeserializeStream = { read };
const definition = new NumberFieldDefinition(NumberFieldType.Uint8);
const struct = new StructValue();
const value = await definition.deserialize(
const value = definition.deserialize(
StructDefaultOptions,
stream,
struct,
@ -77,13 +164,13 @@ describe("Types", () => {
expect(read).lastCalledWith(NumberFieldType.Uint8.size);
});
it("should deserialize Uint16", async () => {
it("should deserialize Uint16", () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]));
const stream: StructDeserializeStream = { read };
const definition = new NumberFieldDefinition(NumberFieldType.Uint16);
const struct = new StructValue();
const value = await definition.deserialize(
const value = definition.deserialize(
StructDefaultOptions,
stream,
struct,
@ -94,13 +181,13 @@ describe("Types", () => {
expect(read).lastCalledWith(NumberFieldType.Uint16.size);
});
it("should deserialize Uint16LE", async () => {
it("should deserialize Uint16LE", () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]));
const stream: StructDeserializeStream = { read };
const definition = new NumberFieldDefinition(NumberFieldType.Uint16);
const struct = new StructValue();
const value = await definition.deserialize(
const value = definition.deserialize(
{ ...StructDefaultOptions, littleEndian: true },
stream,
struct,

View file

@ -1,11 +1,19 @@
// cspell: ignore syncbird
import { StructFieldDefinition, StructFieldValue, StructValue, type StructAsyncDeserializeStream, type StructDeserializeStream, type StructOptions } from '../basic/index.js';
import { SyncPromise } from "../sync-promise.js";
import type { ValueOrPromise } from "../utils.js";
export type DataViewGetters =
{ [TKey in keyof DataView]: TKey extends `get${string}` ? TKey : never }[keyof DataView];
type NumberTypeDeserializer = (array: Uint8Array, littleEndian: boolean) => number;
const DESERIALIZERS: Record<number, NumberTypeDeserializer> = {
1: (array, littleEndian) =>
array[0]!,
2: (array, littleEndian) =>
((array[1]! << 8) | array[0]!) * (littleEndian as any) |
((array[0]! << 8) | array[1]!) * (!littleEndian as any),
4: (array, littleEndian) =>
((array[3]! << 24) | (array[2]! << 16) | (array[1]! << 8) | array[0]!) * (littleEndian as any) |
((array[0]! << 24) | (array[1]! << 16) | (array[2]! << 8) | array[3]!) * (!littleEndian as any),
};
export type DataViewSetters =
{ [TKey in keyof DataView]: TKey extends `set${string}` ? TKey : never }[keyof DataView];
@ -13,33 +21,39 @@ export type DataViewSetters =
export class NumberFieldType {
public readonly TTypeScriptType!: number;
public readonly signed: boolean;
public readonly size: number;
public readonly dataViewGetter: DataViewGetters;
public readonly deserializer: NumberTypeDeserializer;
public readonly convertSign: (value: number) => number;
public readonly dataViewSetter: DataViewSetters;
public constructor(
size: number,
dataViewGetter: DataViewGetters,
signed: boolean,
convertSign: (value: number) => number,
dataViewSetter: DataViewSetters
) {
this.size = size;
this.dataViewGetter = dataViewGetter;
this.signed = signed;
this.deserializer = DESERIALIZERS[size]!;
this.convertSign = convertSign;
this.dataViewSetter = dataViewSetter;
}
public static readonly Int8 = new NumberFieldType(1, 'getInt8', 'setInt8');
public static readonly Int8 = new NumberFieldType(1, true, value => value << 24 >> 24, 'setInt8');
public static readonly Uint8 = new NumberFieldType(1, 'getUint8', 'setUint8');
public static readonly Uint8 = new NumberFieldType(1, false, value => value, 'setUint8');
public static readonly Int16 = new NumberFieldType(2, 'getInt16', 'setInt16');
public static readonly Int16 = new NumberFieldType(2, true, value => value << 16 >> 16, 'setInt16');
public static readonly Uint16 = new NumberFieldType(2, 'getUint16', 'setUint16');
public static readonly Uint16 = new NumberFieldType(2, false, value => value, 'setUint16');
public static readonly Int32 = new NumberFieldType(4, 'getInt32', 'setInt32');
public static readonly Int32 = new NumberFieldType(4, true, value => value, 'setInt32');
public static readonly Uint32 = new NumberFieldType(4, 'getUint32', 'setUint32');
public static readonly Uint32 = new NumberFieldType(4, false, value => value >>> 0, 'setUint32');
}
export class NumberFieldDefinition<
@ -88,11 +102,9 @@ export class NumberFieldDefinition<
return stream.read(this.getSize());
})
.then(array => {
const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
const value = view[this.type.dataViewGetter](
0,
options.littleEndian
);
let value: number;
value = this.type.deserializer(array, options.littleEndian);
value = this.type.convertSign(value);
return this.create(options, struct, value as any);
})
.valueOrPromise();

View file

@ -1,3 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { placeholder } from './utils.js';
describe('placeholder', () => {

View file

@ -0,0 +1,3 @@
{
"extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json"
}

View file

@ -1,3 +1,10 @@
{
"extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json",
"references": [
{
"path": "./tsconfig.test.json"
},
{
"path": "./tsconfig.build.json"
},
]
}

View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"types": [
],
},
"exclude": []
}

View file

@ -16,8 +16,7 @@
"@types/node": "^17.0.17"
},
"dependencies": {
"json5": "^2.2.0",
"@types/jest": "^27.4.1"
"json5": "^2.2.0"
},
"peerDependencies": {
"typescript": "^4.0.0"