refactor(struct): type plugin system rewrok v2

This commit is contained in:
Simon Chan 2021-01-06 18:16:26 +08:00
parent f9502f0e6f
commit c37e3dd953
32 changed files with 865 additions and 729 deletions

View file

@ -22,8 +22,9 @@ const AdbReverseStringResponse =
.string('content', { lengthField: 'length' });
const AdbReverseErrorResponse =
AdbReverseStringResponse
.afterParsed((value) => {
new Struct({ littleEndian: true })
.fields(AdbReverseStringResponse)
.postDeserialize((value) => {
throw new Error(value.content);
});

View file

@ -1,12 +1,12 @@
import { StructValueType } from '@yume-chan/struct';
import { Struct, StructValueType } from '@yume-chan/struct';
import { AdbBufferedStream } from '../../stream';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response';
import { AdbSyncLstatResponse } from './stat';
export const AdbSyncEntryResponse =
AdbSyncLstatResponse
.afterParsed()
new Struct({ littleEndian: true })
.fields(AdbSyncLstatResponse)
.uint32('nameLength')
.string('name', { lengthField: 'nameLength' })
.extra({ id: AdbSyncResponseId.Entry as const });

View file

@ -18,7 +18,8 @@ export const AdbSyncNumberRequest =
.uint32('arg');
export const AdbSyncDataRequest =
AdbSyncNumberRequest
new Struct({ littleEndian: true })
.fields(AdbSyncNumberRequest)
.arrayBuffer('data', { lengthField: 'arg' });
export async function adbSyncWriteRequest(

View file

@ -35,7 +35,7 @@ export const AdbSyncFailResponse =
new Struct({ littleEndian: true })
.uint32('messageLength')
.string('message', { lengthField: 'messageLength' })
.afterParsed(object => {
.postDeserialize(object => {
throw new Error(object.message);
});

View file

@ -20,7 +20,7 @@ export const AdbSyncLstatResponse =
get type() { return this.mode >> 12 as LinuxFileType; },
get permission() { return this.mode & 0b00001111_11111111; },
})
.afterParsed((object) => {
.postDeserialize((object) => {
if (object.mode === 0 &&
object.size === 0 &&
object.mtime === 0
@ -56,7 +56,7 @@ export enum AdbSyncStatErrorCode {
export const AdbSyncStatResponse =
new Struct({ littleEndian: true })
.uint32('error', undefined, placeholder<AdbSyncStatErrorCode>())
.uint32('error', placeholder<AdbSyncStatErrorCode>())
.uint64('dev')
.uint64('ino')
.uint32('mode')
@ -72,7 +72,7 @@ export const AdbSyncStatResponse =
get type() { return this.mode >> 12 as LinuxFileType; },
get permission() { return this.mode & 0b00001111_11111111; },
})
.afterParsed((object) => {
.postDeserialize((object) => {
if (object.error) {
throw new Error(AdbSyncStatErrorCode[object.error]);
}

View file

@ -13,7 +13,7 @@ export enum AdbCommand {
const AdbPacketHeader =
new Struct({ littleEndian: true })
.uint32('command', undefined)
.uint32('command')
.uint32('arg0')
.uint32('arg1')
.uint32('payloadLength')
@ -21,7 +21,8 @@ const AdbPacketHeader =
.int32('magic');
const AdbPacketStruct =
AdbPacketHeader
new Struct({ littleEndian: true })
.fields(AdbPacketHeader)
.arrayBuffer('payload', { lengthField: 'payloadLength' });
export type AdbPacket = StructValueType<typeof AdbPacketStruct>;

View file

@ -27,6 +27,7 @@ interface Progress {
export const Install = withDisplayName('Install')(({
device,
}: RouteProps): JSX.Element => {
const [installing, setInstalling] = useState(false);
const [progress, setProgress] = useState<Progress>();
const handleOpen = useCallback(async () => {
@ -35,6 +36,7 @@ export const Install = withDisplayName('Install')(({
return;
}
setInstalling(true);
setProgress({
filename: file.name,
stage: Stage.Uploading,
@ -70,13 +72,14 @@ export const Install = withDisplayName('Install')(({
totalSize: file.size,
value: 1,
});
setInstalling(false);
}, [device]);
return (
<>
<Stack horizontal>
<DefaultButton
disabled={!device || !!progress}
disabled={!device || installing}
text="Open"
onClick={handleOpen}
/>

View file

@ -16,7 +16,7 @@ export enum ScrcpyControlMessageType {
export const ScrcpySimpleControlMessage =
new Struct()
.uint8('type', undefined, placeholder<ScrcpyControlMessageType.BackOrScreenOn>());
.uint8('type', placeholder<ScrcpyControlMessageType.BackOrScreenOn>());
export type ScrcpySimpleControlMessage = StructInitType<typeof ScrcpySimpleControlMessage>;
@ -38,8 +38,8 @@ export enum AndroidMotionEventAction {
export const ScrcpyInjectTouchControlMessage =
new Struct()
.uint8('type', undefined, ScrcpyControlMessageType.InjectTouch as const)
.uint8('action', undefined, placeholder<AndroidMotionEventAction>())
.uint8('type', ScrcpyControlMessageType.InjectTouch as const)
.uint8('action', placeholder<AndroidMotionEventAction>())
.uint64('pointerId')
.uint32('pointerX')
.uint32('pointerY')
@ -52,7 +52,7 @@ export type ScrcpyInjectTouchControlMessage = StructInitType<typeof ScrcpyInject
export const ScrcpyInjectTextControlMessage =
new Struct()
.uint8('type', undefined, ScrcpyControlMessageType.InjectText as const)
.uint8('type', ScrcpyControlMessageType.InjectText as const)
.uint32('length')
.string('text', { lengthField: 'length' });
@ -99,8 +99,8 @@ export enum AndroidKeyCode {
export const ScrcpyInjectKeyCodeControlMessage =
new Struct()
.uint8('type', undefined, ScrcpyControlMessageType.InjectKeycode as const)
.uint8('action', undefined, placeholder<AndroidKeyEventAction>())
.uint8('type', ScrcpyControlMessageType.InjectKeycode as const)
.uint8('action', placeholder<AndroidKeyEventAction>())
.uint32('keyCode')
.uint32('repeat')
.uint32('metaState');

View file

@ -1,13 +1,13 @@
"use strict";
const tslib_1 = require("tslib");
const clean_webpack_plugin_1 = require("clean-webpack-plugin");
const copy_webpack_plugin_1 = tslib_1.__importDefault(require("copy-webpack-plugin"));
const html_webpack_plugin_1 = tslib_1.__importDefault(require("html-webpack-plugin"));
const mini_css_extract_plugin_1 = tslib_1.__importDefault(require("mini-css-extract-plugin"));
const path_1 = tslib_1.__importDefault(require("path"));
const webpack_bundle_analyzer_1 = require("webpack-bundle-analyzer");
const context = path_1.default.resolve(process.cwd());
const plugins = [
var tslib_1 = require("tslib");
var clean_webpack_plugin_1 = require("clean-webpack-plugin");
var copy_webpack_plugin_1 = tslib_1.__importDefault(require("copy-webpack-plugin"));
var html_webpack_plugin_1 = tslib_1.__importDefault(require("html-webpack-plugin"));
var mini_css_extract_plugin_1 = tslib_1.__importDefault(require("mini-css-extract-plugin"));
var path_1 = tslib_1.__importDefault(require("path"));
var webpack_bundle_analyzer_1 = require("webpack-bundle-analyzer");
var context = path_1.default.resolve(process.cwd());
var plugins = [
new clean_webpack_plugin_1.CleanWebpackPlugin(),
new mini_css_extract_plugin_1.default({
filename: '[name].[contenthash].css',
@ -29,10 +29,10 @@ const plugins = [
if (process.env.ANALYZE) {
plugins.push(new webpack_bundle_analyzer_1.BundleAnalyzerPlugin());
}
const config = (env, argv) => ({
var config = function (env, argv) { return ({
mode: 'development',
devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map',
context,
context: context,
target: 'web',
entry: {
index: './src/index.tsx',
@ -43,10 +43,9 @@ const config = (env, argv) => ({
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
// @ts-expect-error typing is not up to date
fallback: { "path": require.resolve("path-browserify") },
},
plugins,
plugins: plugins,
module: {
rules: [
{ test: /\.js$/, enforce: 'pre', use: ['source-map-loader'], },
@ -59,5 +58,5 @@ const config = (env, argv) => ({
contentBase: path_1.default.resolve(context, 'lib'),
port: 9000,
},
});
}); };
module.exports = config;

View file

@ -1,14 +1,9 @@
import { StructDefaultOptions } from './context';
import { GlobalStructFieldRuntimeTypeRegistry } from './registry';
describe('Runtime', () => {
describe('StructDefaultOptions', () => {
it('should have `littleEndian` equals to `false`', () => {
expect(StructDefaultOptions.littleEndian).toBe(false);
});
it('should have `fieldRuntimeTypeRegistry` equals to `GlobalStructFieldRuntimeTypeRegistry`', () => {
expect(StructDefaultOptions.fieldRuntimeTypeRegistry).toBe(GlobalStructFieldRuntimeTypeRegistry);
it('should have `littleEndian` that equals to `false`', () => {
expect(StructDefaultOptions).toHaveProperty('littleEndian', false);
});
});
});

View file

@ -1,6 +1,3 @@
import type { StructFieldRuntimeTypeRegistry } from './registry';
import { GlobalStructFieldRuntimeTypeRegistry } from './registry';
/**
* Context with enough methods to serialize a struct
*/
@ -21,9 +18,10 @@ export interface StructDeserializationContext extends StructSerializationContext
decodeUtf8(buffer: ArrayBuffer): string;
/**
* Read exactly `length` bytes of data from underlying storage.
* Read data from the underlying data source.
*
* Errors can be thrown to indicates end of file or other errors.
* Context should return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it should throw an error.
*/
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
}
@ -35,11 +33,8 @@ export interface StructOptions {
* Default to `false`
*/
littleEndian: boolean;
fieldRuntimeTypeRegistry: StructFieldRuntimeTypeRegistry;
}
export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false,
fieldRuntimeTypeRegistry: GlobalStructFieldRuntimeTypeRegistry,
};

View file

@ -0,0 +1,22 @@
import { FieldDefinition } from './definition';
import { FieldRuntimeValue } from './runtime-value';
describe('FieldDefinition', () => {
describe('new', () => {
it('should save the `options` parameter', () => {
class MockFieldDefinition extends FieldDefinition<number>{
public constructor(options: number) {
super(options);
}
public getSize(): number {
throw new Error('Method not implemented.');
}
public createValue(): FieldRuntimeValue<FieldDefinition<any, any, any>> {
throw new Error('Method not implemented.');
}
}
expect(new MockFieldDefinition(42)).toHaveProperty('options', 42);
});
});
});

View file

@ -0,0 +1,57 @@
import type { StructOptions, StructSerializationContext } from './context';
import type { FieldRuntimeValue } from './runtime-value';
/**
* A field definition is a bridge between its type and its runtime value.
*
* `Struct` record fields in a list of `FieldDefinition`s.
*
* When `Struct#create` or `Struct#deserialize` are called, each field's definition
* crates its own type of `FieldRuntimeValue` to manage the field value in that `Struct` instance.
*
* One `FieldDefinition` can represents multiple similar types, just returns the corresponding
* `FieldRuntimeValue` when `createValue` was called.
*
* @template TOptions TypeScript type of this definition's `options`.
* @template TValueType TypeScript type of this field.
* @template TRemoveFields Optional remove keys from current `Struct`. Should be a union of string literal types.
*/
export abstract class FieldDefinition<
TOptions = void,
TValueType = unknown,
TRemoveFields = never,
> {
public readonly options: TOptions;
/**
* When `T` is a type initiated `FieldDefinition`,
* use `T['valueType']` to retrieve its `TValueType` type parameter
*/
public readonly valueType!: TValueType;
/**
* When `T` is a type initiated `FieldDefinition`,
* use `T['removeFields']` to retrieve its `TRemoveFields` type parameter .
*/
public readonly removeFields!: TRemoveFields;
public constructor(options: TOptions) {
this.options = options;
}
/**
* When implemented in derived classes, returns the static size (or smallest size) of this field.
*
* Actual size can be retrieved from `FieldRuntimeValue#getSize`
*/
public abstract getSize(): number;
/**
* When implemented in derived classes, creates a `FieldRuntimeValue` for the current field definition.
*/
public abstract createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): FieldRuntimeValue;
}

View file

@ -1,26 +0,0 @@
export enum BuiltInFieldType {
Number,
FixedLengthArrayBufferLike,
VariableLengthArrayBufferLike,
}
export interface FieldDescriptorBaseOptions {
}
export interface FieldDescriptorBase<
TName extends string = string,
TResultObject = {},
TInitObject = {},
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> {
type: BuiltInFieldType | string;
name: TName;
options: TOptions;
resultObject?: TResultObject;
initObject?: TInitObject;
}

View file

@ -1,5 +1,4 @@
export * from './context';
export * from './descriptor';
export * from './registry';
export * from './runtime-type';
export * from './definition';
export * from './runtime-value';
export * from './runtime-object';

View file

@ -1,61 +0,0 @@
import { StructDeserializationContext, StructSerializationContext } from './context';
import { GlobalStructFieldRuntimeTypeRegistry, StructFieldRuntimeTypeRegistry } from './registry';
import { FieldRuntimeValue } from './runtime-type';
describe('Runtime', () => {
describe('StructFieldRuntimeTypeRegistry', () => {
it('should be able to get registered type', () => {
const registry = new StructFieldRuntimeTypeRegistry();
const type = 'mock';
const MockFieldRuntimeValue = class extends FieldRuntimeValue {
static getSize() { return 0; }
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
};
registry.register(type, MockFieldRuntimeValue);
expect(registry.get(type)).toBe(MockFieldRuntimeValue);
});
it('should throw an error if same type been registered twice', () => {
const registry = new StructFieldRuntimeTypeRegistry();
const type = 'mock';
const MockFieldRuntimeValue = class extends FieldRuntimeValue {
static getSize() { return 0; }
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
};
registry.register(type, MockFieldRuntimeValue);
expect(() => registry.register(type, MockFieldRuntimeValue)).toThrowError();
});
});
describe('GlobalStructFieldRuntimeTypeRegistry', () => {
it('should be defined', () => {
expect(GlobalStructFieldRuntimeTypeRegistry).toBeInstanceOf(StructFieldRuntimeTypeRegistry);
});
});
});

View file

@ -1,26 +0,0 @@
import type { BuiltInFieldType } from './descriptor';
import type { FieldRuntimeType } from './runtime-type';
export class StructFieldRuntimeTypeRegistry {
private store: Record<number | string, FieldRuntimeType> = {};
public get(
type: BuiltInFieldType | string
): FieldRuntimeType {
return this.store[type];
}
public register<
TConstructor extends FieldRuntimeType<any>
>(
type: BuiltInFieldType | string,
Constructor: TConstructor
): void {
if (type in this.store) {
throw new Error(`Struct field runtime type '${type}' has already been registered`);
}
this.store[type] = Constructor;
}
}
export const GlobalStructFieldRuntimeTypeRegistry = new StructFieldRuntimeTypeRegistry();

View file

@ -0,0 +1,38 @@
import { createRuntimeObject, getRuntimeValue, setRuntimeValue } from './runtime-object';
describe('RuntimeObject', () => {
describe('createRuntimeObject', () => {
it('should create a special object', () => {
const object = createRuntimeObject();
expect(Object.getOwnPropertySymbols(object)).toHaveLength(1);
});
});
describe('getRuntimeValue', () => {
it('should return previously set value', () => {
const object = createRuntimeObject();
const field = 'foo';
const value = {} as any;
setRuntimeValue(object, field, value);
expect(getRuntimeValue(object, field)).toBe(value);
});
});
describe('setRuntimeValue', () => {
it('should define a proxy property to underlying `RuntimeValue`', () => {
const object = createRuntimeObject();
const field = 'foo';
const getter = jest.fn(() => 42);
const setter = jest.fn((value: number) => { });
const value = { get: getter, set: setter } as any;
setRuntimeValue(object, field, value);
expect((object as any)[field]).toBe(42);
expect(getter).toBeCalledTimes(1);
(object as any)[field] = 100;
expect(setter).toBeCalledTimes(1);
expect(setter).lastCalledWith(100);
});
});
});

View file

@ -0,0 +1,36 @@
import { FieldRuntimeValue } from './runtime-value';
const RuntimeValues = Symbol('RuntimeValues');
export interface RuntimeObject {
[RuntimeValues]: Record<PropertyKey, FieldRuntimeValue>;
}
/** Creates a new runtime object that can be used with `getRuntimeValue` and `setRuntimeValue` */
export function createRuntimeObject(): RuntimeObject {
return {
[RuntimeValues]: {},
};
}
/** Gets the previously set `RuntimeValue` for specified `key` on `object` */
export function getRuntimeValue(object: RuntimeObject, key: PropertyKey): FieldRuntimeValue {
return object[RuntimeValues][key as any] as FieldRuntimeValue;
}
/**
* Sets the `RuntimeValue` for specified `key` on `object`,
* also sets up property accessors so reads/writes to `object`'s `key` will be forwarded to
* the underlying `RuntimeValue`
*/
export function setRuntimeValue(object: RuntimeObject, key: PropertyKey, runtimeValue: FieldRuntimeValue): void {
delete (object as any)[key];
object[RuntimeValues][key as any] = runtimeValue;
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get() { return runtimeValue.get(); },
set(value) { runtimeValue.set(value); },
});
}

View file

@ -1,27 +0,0 @@
import { StructDeserializationContext, StructSerializationContext } from './context';
import { FieldRuntimeValue } from './runtime-type';
describe('Runtime', () => {
describe('FieldRuntimeValue', () => {
it('`getSize` should return same value as static `getSize`', () => {
class MockFieldRuntimeValue extends FieldRuntimeValue {
static getSize() { return 42; }
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
const instance = new MockFieldRuntimeValue(undefined as any, undefined as any, undefined as any, undefined as any);
expect(instance.getSize()).toBe(42);
});
});
});

View file

@ -1,45 +0,0 @@
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { FieldDescriptorBase } from './descriptor';
export abstract class FieldRuntimeValue<TDescriptor extends FieldDescriptorBase = FieldDescriptorBase> {
public readonly descriptor: TDescriptor;
public readonly options: Readonly<StructOptions>;
public readonly context: StructSerializationContext;
public constructor(
descriptor: TDescriptor,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
) {
this.descriptor = descriptor;
this.options = options;
this.context = context;
}
public abstract deserialize(context: StructDeserializationContext, object: any): void | Promise<void>;
public getSize(): number {
const Constructor = Object.getPrototypeOf(this).constructor as FieldRuntimeType<TDescriptor>;
return Constructor.getSize(this.descriptor, this.options);
}
public abstract get(): unknown;
public abstract set(value: unknown): void;
public abstract serialize(dataView: DataView, offset: number, context: StructSerializationContext): void;
}
export interface FieldRuntimeType<TDescriptor extends FieldDescriptorBase = FieldDescriptorBase> {
new(
descriptor: TDescriptor,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
): FieldRuntimeValue<TDescriptor>;
getSize(descriptor: TDescriptor, options: Readonly<StructOptions>): number;
}

View file

@ -1,34 +1,67 @@
import { createObjectWithRuntimeValues, getRuntimeValue, setRuntimeValue } from './runtime-value';
import { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import { FieldDefinition } from './definition';
import { FieldRuntimeValue } from './runtime-value';
describe('Runtime', () => {
describe('RuntimeValue', () => {
it('`createObjectWithRuntimeValues` should create an object with symbol', () => {
const object = createObjectWithRuntimeValues();
expect(Object.getOwnPropertySymbols(object)).toHaveLength(1);
describe('FieldRuntimeValue', () => {
describe('.constructor', () => {
it('should save parameters', () => {
class MockFieldRuntimeValue extends FieldRuntimeValue {
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
const definition = 1 as any;
const options = 2 as any;
const context = 3 as any;
const object = 4 as any;
const fieldRuntimeValue = new MockFieldRuntimeValue(definition, options, context, object);
expect(fieldRuntimeValue).toHaveProperty('definition', definition);
expect(fieldRuntimeValue).toHaveProperty('options', options);
expect(fieldRuntimeValue).toHaveProperty('context', context);
expect(fieldRuntimeValue).toHaveProperty('object', object);
});
});
it('`getRuntimeValue` should return previously set value', () => {
const object = createObjectWithRuntimeValues();
const field = 'foo';
const value = {} as any;
setRuntimeValue(object, field, value);
expect(getRuntimeValue(object, field)).toBe(value);
});
describe('#getSize', () => {
it('should return same value as definition\'s', () => {
class MockFieldDefinition extends FieldDefinition {
public getSize(): number {
return 42;
}
public createValue(options: Readonly<StructOptions>, context: StructSerializationContext, object: any): FieldRuntimeValue<FieldDefinition<any, any, any>> {
throw new Error('Method not implemented.');
}
}
it('`setRuntimeValue` should define a proxy to underlying `RuntimeValue`', () => {
const object = createObjectWithRuntimeValues();
const field = 'foo';
const getter = jest.fn(() => 42);
const setter = jest.fn((value: number) => { });
const value = { get: getter, set: setter } as any;
setRuntimeValue(object, field, value);
class MockFieldRuntimeValue extends FieldRuntimeValue {
public deserialize(context: StructDeserializationContext): void | Promise<void> {
throw new Error('Method not implemented.');
}
public get(): unknown {
throw new Error('Method not implemented.');
}
public set(value: unknown): void {
throw new Error('Method not implemented.');
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
throw new Error('Method not implemented.');
}
}
expect((object as any)[field]).toBe(42);
expect(getter).toBeCalledTimes(1);
(object as any)[field] = 100;
expect(setter).toBeCalledTimes(1);
expect(setter).lastCalledWith(100);
const fieldDefinition = new MockFieldDefinition();
const fieldRuntimeValue = new MockFieldRuntimeValue(fieldDefinition, undefined as any, undefined as any, undefined as any);
expect(fieldRuntimeValue.getSize()).toBe(42);
});
});
});

View file

@ -1,28 +1,69 @@
import { FieldRuntimeValue } from './runtime-type';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { FieldDefinition } from './definition';
const RuntimeValues = Symbol('RuntimeValues');
/**
* Field runtime value manages one field of one `Struct` instance.
*
* If one `FieldDefinition` needs to change other field's semantics
* It can override other fields' `FieldRuntimeValue` in its own `FieldRuntimeValue`'s constructor
*/
export abstract class FieldRuntimeValue<
TDefinition extends FieldDefinition<any, any, any> = FieldDefinition<any, any, any>
> {
/** Gets the definition associated with this runtime value */
public readonly definition: TDefinition;
export interface WithRuntimeValues {
[RuntimeValues]: Record<string, FieldRuntimeValue>;
}
export function createObjectWithRuntimeValues(): WithRuntimeValues {
const object = {} as any;
object[RuntimeValues] = {};
return object;
}
export function getRuntimeValue(object: WithRuntimeValues, field: string): FieldRuntimeValue {
return (object as any)[RuntimeValues][field] as FieldRuntimeValue;
}
export function setRuntimeValue(object: WithRuntimeValues, field: string, runtimeValue: FieldRuntimeValue): void {
(object as any)[RuntimeValues][field] = runtimeValue;
delete (object as any)[field];
Object.defineProperty(object, field, {
configurable: true,
enumerable: true,
get() { return runtimeValue.get(); },
set(value) { runtimeValue.set(value); },
});
/** Gets the options of the associated `Struct` */
public readonly options: Readonly<StructOptions>;
/** Gets the serialization context of the associated `Struct` instance */
public readonly context: StructSerializationContext;
/** Gets the associated `Struct` instance */
public readonly object: any;
public constructor(
definition: TDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
) {
this.definition = definition;
this.options = options;
this.context = context;
this.object = object;
}
/** When implemented in derived classes, deserialize this field from the specified `context` */
public abstract deserialize(
context: StructDeserializationContext
): void | Promise<void>;
/**
* Gets the actual size of this field. By default, the return value of its `definition.getSize()`
*
* When overridden in derived classes, can have custom logic to calculate the actual size.
*/
public getSize(): number {
return this.definition.getSize();
}
/**
* When implemented in derived classes, returns the current value of this field
*/
public abstract get(): unknown;
/**
* When implemented in derived classes, update the current value of this field
*/
public abstract set(value: unknown): void;
/**
* When implemented in derived classes, serializes this field into `dataView` at `offset`
*/
public abstract serialize(
dataView: DataView,
offset: number,
context: StructSerializationContext
): void;
}

View file

@ -1,4 +1,4 @@
export * from './runtime';
export * from './basic';
export * from './struct';
export { default as Struct } from './struct';
export * from './types';

View file

@ -1,5 +1,5 @@
import { BuiltInFieldType, createObjectWithRuntimeValues, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './runtime';
import { ArrayBufferLikeFieldDescriptor, Evaluate, FixedLengthArrayBufferFieldDescriptor, Identity, KeysOfType, NumberFieldDescriptor, NumberFieldSubType, OmitNever, Overwrite, VariableLengthArrayBufferFieldDescriptor } from './types';
import { createRuntimeObject, FieldDefinition, FieldRuntimeValue, getRuntimeValue, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldType, Evaluate, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, Identity, KeysOfType, NumberFieldDefinition, NumberFieldType, Overwrite, StringFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
/**
* Extract the value type of the specified `Struct`
@ -12,7 +12,7 @@ export type StructValueType<T> =
/**
* Extract the init type of the specified `Struct`
*/
export type StructInitType<T extends Struct<object, object, object, unknown>> =
export type StructInitType<T extends Struct<any, object, object, any>> =
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
/**
@ -22,35 +22,28 @@ type AddFieldDescriptor<
TValue extends object,
TInit extends object,
TExtra extends object,
TAfterParsed,
TDescriptor extends FieldDescriptorBase> =
TPostDeserialized,
TFieldName extends PropertyKey,
TDefinition extends FieldDefinition<any, any, any>> =
Identity<Struct<
// Merge two types
Evaluate<
TValue &
// `TDescriptor.resultObject` is optional, so remove `undefined` from its type
Exclude<TDescriptor['resultObject'], undefined>
>,
// `TDescriptor.initObject` signals removal of fields by setting its type to `never`
// I don't `Evaluate` here, because if I do, the result type will become too complex,
// and TypeScript will refuse to evaluate it.
OmitNever<
TInit &
// `TDescriptor.initObject` is optional, so remove `undefined` from its type
Exclude<TDescriptor['initObject'], undefined>
>,
// Evaluate immediately to optimize editor hover tooltip
Evaluate<TValue & Record<TFieldName, TDefinition['valueType']>>,
// There is no `Evaluate` here, because otherwise the type of a `Struct` with many fields
// can become too complex for TypeScript to compute
Evaluate<Omit<TInit, TDefinition['removeFields']> & Record<TFieldName, TDefinition['valueType']>>,
TExtra,
TAfterParsed
TPostDeserialized
>>;
/**
* Overload methods to add an array typed field
* Overload methods to add an array buffer like field
*/
interface AddArrayBufferFieldDescriptor<
interface ArrayBufferLikeFieldCreator<
TValue extends object,
TInit extends object,
TExtra extends object,
TAfterParsed
TPostDeserialized
> {
/**
* Append a fixed-length array to the `Struct`
@ -62,23 +55,23 @@ interface AddArrayBufferFieldDescriptor<
* For example, if this field is a string, you can declare it as a string enum or literal union.
*/
<
TName extends string,
TType extends ArrayBufferLikeFieldDescriptor.SubType,
TTypeScriptType extends ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
TName extends PropertyKey,
TType extends ArrayBufferLikeFieldType,
TTypeScriptType = TType['valueType'],
>(
name: TName,
type: TType,
options: FixedLengthArrayBufferFieldDescriptor.Options,
options: FixedLengthArrayBufferLikeFieldOptions,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TValue,
TInit,
TExtra,
TAfterParsed,
FixedLengthArrayBufferFieldDescriptor<
TPostDeserialized,
TName,
FixedLengthArrayBufferLikeFieldDefinition<
TType,
TTypeScriptType
FixedLengthArrayBufferLikeFieldOptions
>
>;
@ -86,342 +79,350 @@ interface AddArrayBufferFieldDescriptor<
* Append a variable-length array to the `Struct`
*/
<
TName extends string,
TType extends ArrayBufferLikeFieldDescriptor.SubType,
TLengthField extends KeysOfType<TInit, number | string>,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
TName extends PropertyKey,
TType extends ArrayBufferLikeFieldType,
TOptions extends VariableLengthArrayBufferLikeFieldOptions<TInit>,
TTypeScriptType = TType['valueType'],
>(
name: TName,
type: TType,
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>,
options: TOptions,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TValue,
TInit,
TExtra,
TAfterParsed,
VariableLengthArrayBufferFieldDescriptor<
TPostDeserialized,
TName,
VariableLengthArrayBufferLikeFieldDefinition<
TType,
TInit,
TLengthField,
TTypeScriptType
TOptions
>
>;
}
interface AddArrayBufferSubTypeFieldDescriptor<
TResult extends object,
/**
* Similar to `ArrayBufferLikeFieldCreator`, but bind to a `ArrayBufferLikeFieldType`
*/
interface ArrayBufferTypeFieldDefinitionCreator<
TValue extends object,
TInit extends object,
TExtra extends object,
TAfterParsed,
TType extends ArrayBufferLikeFieldDescriptor.SubType
TPostDeserialized,
TType extends ArrayBufferLikeFieldType
> {
<
TName extends string,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
TName extends PropertyKey,
TTypeScriptType = TType['valueType'],
>(
name: TName,
options: FixedLengthArrayBufferFieldDescriptor.Options,
options: FixedLengthArrayBufferLikeFieldOptions,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TResult,
TValue,
TInit,
TExtra,
TAfterParsed,
FixedLengthArrayBufferFieldDescriptor<
TPostDeserialized,
TName,
FixedLengthArrayBufferLikeFieldDefinition<
TType,
TTypeScriptType
FixedLengthArrayBufferLikeFieldOptions
>
>;
<
TName extends string,
TName extends PropertyKey,
TLengthField extends KeysOfType<TInit, number | string>,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
TOptions extends VariableLengthArrayBufferLikeFieldOptions<TInit, TLengthField>,
TTypeScriptType = TType['valueType'],
>(
name: TName,
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>,
_typescriptType?: TTypeScriptType,
options: TOptions,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TResult,
TValue,
TInit,
TExtra,
TAfterParsed,
VariableLengthArrayBufferFieldDescriptor<
TPostDeserialized,
TName,
VariableLengthArrayBufferLikeFieldDefinition<
TType,
TInit,
TLengthField,
TTypeScriptType
TOptions
>
>;
}
export type StructAfterParsed<TResult, TAfterParsed> =
(this: TResult, object: TResult) => TAfterParsed;
export type StructPostDeserialized<TValue, TPostDeserialized> =
(this: TValue, object: TValue) => TPostDeserialized;
export default class Struct<
TResult extends object = {},
TValue extends object = {},
TInit extends object = {},
TExtra extends object = {},
TAfterParsed = undefined,
TPostDeserialized = undefined,
> {
public readonly valueType!: TValue;
public readonly initType!: TInit;
public readonly extraType!: TExtra;
public readonly options: Readonly<StructOptions>;
private _size = 0;
/**
* Get the static size (exclude fields that can change size at runtime)
*/
public get size() { return this._size; }
private fieldDescriptors: FieldDescriptorBase[] = [];
private _fields: [name: PropertyKey, definition: FieldDefinition<any, any, any>][] = [];
private _extra: PropertyDescriptorMap = {};
private _afterParsed?: StructAfterParsed<any, any>;
private _postDeserialized?: StructPostDeserialized<TValue, any>;
public constructor(options?: Partial<StructOptions>) {
this.options = { ...StructDefaultOptions, ...options };
}
private clone(): Struct<any, any, any, any> {
const result = new Struct<any, any, any, any>(this.options);
result.fieldDescriptors = this.fieldDescriptors.slice();
result._size = this._size;
result._extra = this._extra;
result._afterParsed = this._afterParsed;
return result;
public field<
TName extends PropertyKey,
TDefinition extends FieldDefinition<any, any, any>
>(
name: TName,
definition: TDefinition,
): AddFieldDescriptor<
TValue,
TInit,
TExtra,
TPostDeserialized,
TName,
TDefinition
> {
this._fields.push([name, definition]);
const size = definition.getSize();
this._size += size;
// Force cast `this` to another type
return this as any;
}
public field<TDescriptor extends FieldDescriptorBase>(
descriptor: TDescriptor,
): AddFieldDescriptor<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
const result = this.clone();
result.fieldDescriptors.push(descriptor);
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
const size = Constructor.getSize(descriptor, this.options);
result._size += size;
return result;
public fields<TOther extends Struct<any, any, any, any>>(
struct: TOther
): Struct<
TValue & TOther['valueType'],
TInit & TOther['initType'],
TExtra & TOther['extraType'],
TPostDeserialized
> {
for (const field of struct._fields) {
this._fields.push(field);
}
this._size += struct._size;
Object.assign(this._extra, struct._extra);
return this as any;
}
private number<
TName extends string,
TType extends NumberFieldSubType = NumberFieldSubType,
TTypeScriptType = TType['value']
TName extends PropertyKey,
TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = TType['valueType']
>(
name: TName,
type: TType,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.field<NumberFieldDescriptor<TName, TType, TTypeScriptType>>({
type: BuiltInFieldType.Number,
return this.field(
name,
subType: type,
options,
});
new NumberFieldDefinition(type, _typescriptType),
);
}
public uint8<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Uint8']['value']
TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldType)['Uint8']['valueType']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Uint8,
options,
NumberFieldType.Uint8,
_typescriptType
);
}
public uint16<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Uint16']['value']
TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldType)['Uint16']['valueType']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Uint16,
options,
NumberFieldType.Uint16,
_typescriptType
);
}
public int32<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Int32']['value']
TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldType)['Int32']['valueType']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Int32,
options,
NumberFieldType.Int32,
_typescriptType
);
}
public uint32<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Uint32']['value']
TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldType)['Uint32']['valueType']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Uint32,
options,
NumberFieldType.Uint32,
typescriptType
);
}
public int64<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Int64']['value']
TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldType)['Int64']['valueType']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Int64,
options,
NumberFieldType.Int64,
_typescriptType
);
}
public uint64<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Uint64']['value']
TName extends PropertyKey,
TTypeScriptType = (typeof NumberFieldType)['Uint64']['valueType']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Uint64,
options,
NumberFieldType.Uint64,
_typescriptType
);
}
private arrayBufferLike: AddArrayBufferFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
name: string,
type: ArrayBufferLikeFieldDescriptor.SubType,
options: FixedLengthArrayBufferFieldDescriptor.Options | VariableLengthArrayBufferFieldDescriptor.Options
): Struct<any, any, any, any> => {
private arrayBufferLike: ArrayBufferLikeFieldCreator<TValue, TInit, TExtra, TPostDeserialized> = (
name: PropertyKey,
type: ArrayBufferLikeFieldType,
options: FixedLengthArrayBufferLikeFieldOptions | VariableLengthArrayBufferLikeFieldOptions
): any => {
if ('length' in options) {
return this.field<FixedLengthArrayBufferFieldDescriptor>({
type: BuiltInFieldType.FixedLengthArrayBufferLike,
return this.field(
name,
subType: type,
options: options,
});
new FixedLengthArrayBufferLikeFieldDefinition(type, options),
);
} else {
return this.field<VariableLengthArrayBufferFieldDescriptor>({
type: BuiltInFieldType.VariableLengthArrayBufferLike,
return this.field(
name,
subType: type,
options: options,
});
new VariableLengthArrayBufferLikeFieldDefinition(type, options),
);
}
};
public arrayBuffer: AddArrayBufferSubTypeFieldDescriptor<
TResult,
public arrayBuffer: ArrayBufferTypeFieldDefinitionCreator<
TValue,
TInit,
TExtra,
TAfterParsed,
ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer
> = <TName extends string>(
name: TName,
TPostDeserialized,
ArrayBufferFieldType
> = (
name: PropertyKey,
options: any
) => {
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer, options);
): any => {
return this.arrayBufferLike(name, ArrayBufferFieldType.instance, options);
};
public string: AddArrayBufferSubTypeFieldDescriptor<
TResult,
public string: ArrayBufferTypeFieldDefinitionCreator<
TValue,
TInit,
TExtra,
TAfterParsed,
ArrayBufferLikeFieldDescriptor.SubType.String
> = <TName extends string>(
name: TName,
TPostDeserialized,
StringFieldType
> = (
name: PropertyKey,
options: any
) => {
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.String, options);
): any => {
return this.arrayBufferLike(name, StringFieldType.instance, options);
};
public extra<TValue extends Record<
public extra<T extends Record<
// This trick disallows any keys that are already in `TValue`
Exclude<
keyof TValue,
Exclude<keyof TValue, keyof TResult>>,
keyof T,
Exclude<keyof T, keyof TValue>
>,
never
>>(
value: TValue & ThisType<Overwrite<Overwrite<TExtra, TValue>, TResult>>
value: T & ThisType<Overwrite<Overwrite<TExtra, T>, TValue>>
): Struct<
TResult,
TValue,
TInit,
Overwrite<TExtra, TValue>,
TAfterParsed
Overwrite<TExtra, T>,
TPostDeserialized
> {
const result = this.clone();
result._extra = { ...result._extra, ...Object.getOwnPropertyDescriptors(value) };
return result;
Object.assign(this._extra, Object.getOwnPropertyDescriptors(value));
return this as any;
}
public afterParsed(
callback: StructAfterParsed<TResult, never>
): Struct<TResult, TInit, TExtra, never>;
public afterParsed(
callback?: StructAfterParsed<TResult, void>
): Struct<TResult, TInit, TExtra, undefined>;
public afterParsed<TAfterParsed>(
callback?: StructAfterParsed<TResult, TAfterParsed>
): Struct<TResult, TInit, TExtra, TAfterParsed>;
public afterParsed(
callback?: StructAfterParsed<TResult, any>
/**
*
*/
public postDeserialize(
callback: StructPostDeserialized<TValue, never>
): Struct<TValue, TInit, TExtra, never>;
public postDeserialize(
callback?: StructPostDeserialized<TValue, void>
): Struct<TValue, TInit, TExtra, undefined>;
public postDeserialize<TPostSerialize>(
callback?: StructPostDeserialized<TValue, TPostSerialize>
): Struct<TValue, TInit, TExtra, TPostSerialize>;
public postDeserialize(
callback?: StructPostDeserialized<TValue, any>
) {
const result = this.clone();
result._afterParsed = callback;
return result;
this._postDeserialized = callback;
return this as any;
}
private initializeObject(context: StructSerializationContext) {
const object = createObjectWithRuntimeValues();
const object = createRuntimeObject();
Object.defineProperties(object, this._extra);
for (const descriptor of this.fieldDescriptors) {
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
const runtimeValue = new Constructor(descriptor, this.options, context, object);
setRuntimeValue(object, descriptor.name, runtimeValue);
for (const [name, definition] of this._fields) {
const runtimeValue = definition.createValue(this.options, context, object);
setRuntimeValue(object, name, runtimeValue);
}
return object;
}
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> {
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TValue> {
const object = this.initializeObject(context);
for (const { name: fieldName } of this.fieldDescriptors) {
const runtimeValue = getRuntimeValue(object, fieldName);
runtimeValue.set((init as any)[fieldName]);
for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, name);
runtimeValue.set((init as any)[name]);
}
return object as any;
@ -429,16 +430,16 @@ export default class Struct<
public async deserialize(
context: StructDeserializationContext
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
): Promise<TPostDeserialized extends undefined ? Overwrite<TExtra, TValue> : TPostDeserialized> {
const object = this.initializeObject(context);
for (const { name: fieldName } of this.fieldDescriptors) {
const runtimeValue = getRuntimeValue(object, fieldName);
await runtimeValue.deserialize(context, object);
for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, name);
await runtimeValue.deserialize(context);
}
if (this._afterParsed) {
const result = this._afterParsed.call(object, object);
if (this._postDeserialized) {
const result = this._postDeserialized.call(object as TValue, object as TValue);
if (result) {
return result;
}
@ -453,8 +454,8 @@ export default class Struct<
let structSize = 0;
const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = [];
for (const { name: fieldName } of this.fieldDescriptors) {
const runtimeValue = getRuntimeValue(object, fieldName);
for (const [name] of this._fields) {
const runtimeValue = getRuntimeValue(object, name);
const size = runtimeValue.getSize();
fieldsInfo.push({ runtimeValue, size });
structSize += size;

View file

@ -0,0 +1,79 @@
import { StructDeserializationContext, StructSerializationContext } from '../basic';
import { ArrayBufferFieldType, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
describe('Types', () => {
describe('ArrayBufferFieldType', () => {
it('should have a static instance', () => {
expect(ArrayBufferFieldType.instance).toBeInstanceOf(ArrayBufferFieldType);
});
it('`toArrayBuffer` should return the same `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.toArrayBuffer(arrayBuffer)).toBe(arrayBuffer);
});
it('`fromArrayBuffer` should return the same `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.fromArrayBuffer(arrayBuffer)).toBe(arrayBuffer);
});
it('`getSize` should return the `byteLength` of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
expect(ArrayBufferFieldType.instance.getSize(arrayBuffer)).toBe(10);
});
});
describe('Uint8ClampedArrayFieldType', () => {
it('should have a static instance', () => {
expect(Uint8ClampedArrayFieldType.instance).toBeInstanceOf(Uint8ClampedArrayFieldType);
});
it('`toArrayBuffer` should return its `buffer`', () => {
const array = new Uint8ClampedArray(10);
const buffer = array.buffer;
expect(Uint8ClampedArrayFieldType.instance.toArrayBuffer(array)).toBe(buffer);
});
it('`fromArrayBuffer` should return a view of the `ArrayBuffer`', () => {
const arrayBuffer = new ArrayBuffer(10);
const array = Uint8ClampedArrayFieldType.instance.fromArrayBuffer(arrayBuffer);
expect(array).toHaveProperty('buffer', arrayBuffer);
expect(array).toHaveProperty('byteOffset', 0);
expect(array).toHaveProperty('byteLength', 10);
});
});
describe('StringFieldType', () => {
it('should have a static instance', () => {
expect(StringFieldType.instance).toBeInstanceOf(StringFieldType);
});
it('`toArrayBuffer` should return the decoded string', () => {
const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructSerializationContext = {
encodeUtf8(input) {
return Buffer.from(input, 'utf-8');
},
};
expect(StringFieldType.instance.toArrayBuffer(text, context)).toEqual(arrayBuffer);
});
it('`fromArrayBuffer` should return the encoded ArrayBuffer', () => {
const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructDeserializationContext = {
decodeUtf8(arrayBuffer: ArrayBuffer): string {
return Buffer.from(arrayBuffer).toString('utf-8');
},
encodeUtf8(input) {
throw new Error('Method not implemented.');
},
read(length) {
throw new Error('Method not implemented.');
},
};
expect(StringFieldType.instance.fromArrayBuffer(arrayBuffer, context)).toBe(text);
});
});
});

View file

@ -1,135 +1,159 @@
import { FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, StructDeserializationContext, StructSerializationContext } from '../runtime';
import { FieldDefinition, FieldRuntimeValue, StructDeserializationContext, StructSerializationContext } from '../basic';
export namespace ArrayBufferLikeFieldDescriptor {
export enum SubType {
ArrayBuffer,
Uint8ClampedArray,
String,
}
/**
* Base class for all types that
* can be converted from an ArrayBuffer when deserialized,
* and need to be converted to an ArrayBuffer when serializing
*
* @template TType The actual TypeScript type of this type
* @template TTypeScriptType Optional another type (should be compatible with `TType`)
* specified by user when creating field definitions.
*/
export abstract class ArrayBufferLikeFieldType<TType = unknown, TTypeScriptType = TType> {
public readonly valueType!: TTypeScriptType;
export type TypeScriptType<TType extends SubType = SubType> =
TType extends SubType.ArrayBuffer ? ArrayBuffer :
TType extends SubType.Uint8ClampedArray ? Uint8ClampedArray :
TType extends SubType.String ? string :
never;
/**
* When implemented in derived classes, converts the type-specific `value` to an `ArrayBuffer`
*
* This function should be "pure", i.e.,
* same `value` should always be converted to `ArrayBuffer`s that have same content.
*/
public abstract toArrayBuffer(value: TType, context: StructSerializationContext): ArrayBuffer;
/** When implemented in derived classes, converts the `ArrayBuffer` to a type-specific value */
public abstract fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): TType;
/**
* When implemented in derived classes, gets the size in byte of the type-specific `value`.
*
* If the size can't be calculated without first converting the `value` back to an `ArrayBuffer`,
* implementer should returns `-1` so the caller will get its size by first converting it to
* an `ArrayBuffer` (and cache the result).
*/
public abstract getSize(value: TType): number;
}
export interface ArrayBufferLikeFieldDescriptor<
TName extends string = string,
TType extends ArrayBufferLikeFieldDescriptor.SubType = ArrayBufferLikeFieldDescriptor.SubType,
TResultObject = {},
TInitObject = {},
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> extends FieldDescriptorBase<
TName,
TResultObject,
TInitObject,
TOptions
> {
subType: TType;
/** An ArrayBufferLike type that's actually an `ArrayBuffer` */
export class ArrayBufferFieldType extends ArrayBufferLikeFieldType<ArrayBuffer> {
public static readonly instance = new ArrayBufferFieldType();
protected constructor() {
super();
}
public toArrayBuffer(value: ArrayBuffer): ArrayBuffer {
return value;
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer): ArrayBuffer {
return arrayBuffer;
}
public getSize(value: ArrayBuffer): number {
return value.byteLength;
}
}
/** Am ArrayBufferLike type that converts to/from the `ArrayBuffer` from/to a `Uint8ClampedArray` */
export class Uint8ClampedArrayFieldType
extends ArrayBufferLikeFieldType<Uint8ClampedArray, Uint8ClampedArray> {
public static readonly instance = new Uint8ClampedArrayFieldType();
protected constructor() {
super();
}
public toArrayBuffer(value: Uint8ClampedArray): ArrayBuffer {
return value.buffer;
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer): Uint8ClampedArray {
return new Uint8ClampedArray(arrayBuffer);
}
public getSize(value: Uint8ClampedArray): number {
return value.byteLength;
}
}
/** Am ArrayBufferLike type that converts to/from the `ArrayBuffer` from/to a `string` */
export class StringFieldType<TTypeScriptType = string>
extends ArrayBufferLikeFieldType<string, TTypeScriptType> {
public static readonly instance = new StringFieldType();
public toArrayBuffer(value: string, context: StructSerializationContext): ArrayBuffer {
return context.encodeUtf8(value);
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): string {
return context.decodeUtf8(arrayBuffer);
}
public getSize(): number {
// Return `-1`, so `ArrayBufferLikeFieldDefinition` will
// convert this `value` into an `ArrayBuffer` (and cache the result),
// Then get the size from that `ArrayBuffer`
return -1;
}
}
export abstract class ArrayBufferLikeFieldDefinition<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TOptions = void,
TRemoveFields = never,
> extends FieldDefinition<
TOptions,
TType['valueType'],
TRemoveFields
>{
public readonly type: TType;
public constructor(type: TType, options: TOptions) {
super(options);
this.type = type;
}
}
const EmptyArrayBuffer = new ArrayBuffer(0);
const EmptyUint8ClampedArray = new Uint8ClampedArray(EmptyArrayBuffer);
const EmptyString = '';
export abstract class ArrayBufferLikeFieldRuntimeValue<TDescriptor extends ArrayBufferLikeFieldDescriptor>
extends FieldRuntimeValue<TDescriptor> {
export abstract class ArrayBufferLikeFieldRuntimeValue<
TDefinition extends ArrayBufferLikeFieldDefinition<any, any, any>,
> extends FieldRuntimeValue<TDefinition> {
protected arrayBuffer: ArrayBuffer | undefined;
protected typedArray: ArrayBufferView | undefined;
protected typedValue: unknown;
protected string: string | undefined;
protected getDeserializeSize(object: any): number {
protected getDeserializeSize(): number {
return this.getSize();
}
public async deserialize(context: StructDeserializationContext, object: any): Promise<void> {
const size = this.getDeserializeSize(object);
public async deserialize(context: StructDeserializationContext): Promise<void> {
const size = this.getDeserializeSize();
this.arrayBuffer = undefined;
this.typedArray = undefined;
this.string = undefined;
this.typedValue = undefined;
if (size === 0) {
this.arrayBuffer = EmptyArrayBuffer;
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.typedArray = EmptyUint8ClampedArray;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.string = EmptyString;
break;
}
return;
} else {
this.arrayBuffer = await context.read(size);
}
const buffer = await context.read(size);
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
this.arrayBuffer = buffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.arrayBuffer = buffer;
this.typedArray = new Uint8ClampedArray(buffer);
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.arrayBuffer = buffer;
this.string = context.decodeUtf8(buffer);
break;
default:
throw new Error('Unknown type');
}
this.typedValue = this.definition.type.fromArrayBuffer(this.arrayBuffer, context);
}
public get(): unknown {
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
return this.arrayBuffer;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
return this.typedArray;
case ArrayBufferLikeFieldDescriptor.SubType.String:
return this.string;
default:
throw new Error('Unknown type');
}
return this.typedValue;
}
public set(value: unknown): void {
this.typedValue = value;
this.arrayBuffer = undefined;
this.typedArray = undefined;
this.string = undefined;
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
this.arrayBuffer = value as ArrayBuffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.typedArray = value as Uint8ClampedArray;
this.arrayBuffer = this.typedArray.buffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.string = value as string;
break;
default:
throw new Error('Unknown type');
}
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
if (this.descriptor.subType !== ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer &&
!this.arrayBuffer) {
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.arrayBuffer = this.typedArray!.buffer;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.arrayBuffer = context.encodeUtf8(this.string!);
break;
default:
throw new Error('Unknown type');
}
if (!this.arrayBuffer) {
this.arrayBuffer = this.definition.type.toArrayBuffer(this.typedValue, context);
}
new Uint8Array(dataView.buffer)

View file

@ -1,37 +1,33 @@
import { BuiltInFieldType, FieldDescriptorBaseOptions, GlobalStructFieldRuntimeTypeRegistry } from '../runtime';
import { ArrayBufferLikeFieldDescriptor, ArrayBufferLikeFieldRuntimeValue } from './array-buffer';
import { StructOptions, StructSerializationContext } from '../basic';
import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldRuntimeValue, ArrayBufferLikeFieldType } from './array-buffer';
export namespace FixedLengthArrayBufferFieldDescriptor {
export interface Options extends FieldDescriptorBaseOptions {
export interface FixedLengthArrayBufferLikeFieldOptions {
length: number;
}
}
export interface FixedLengthArrayBufferFieldDescriptor<
TName extends string = string,
TType extends ArrayBufferLikeFieldDescriptor.SubType = ArrayBufferLikeFieldDescriptor.SubType,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>,
TOptions extends FixedLengthArrayBufferFieldDescriptor.Options = FixedLengthArrayBufferFieldDescriptor.Options
> extends ArrayBufferLikeFieldDescriptor<
TName,
export class FixedLengthArrayBufferLikeFieldDefinition<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TOptions extends FixedLengthArrayBufferLikeFieldOptions = FixedLengthArrayBufferLikeFieldOptions,
> extends ArrayBufferLikeFieldDefinition<
TType,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions
> {
type: BuiltInFieldType.FixedLengthArrayBufferLike;
public getSize(): number {
return this.options.length;
}
options: TOptions;
public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): FixedLengthArrayBufferFieldRuntimeValue {
return new FixedLengthArrayBufferFieldRuntimeValue(this, options, context, object);
}
};
class FixedLengthArrayBufferFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<FixedLengthArrayBufferFieldDescriptor>{
public static getSize(descriptor: FixedLengthArrayBufferFieldDescriptor) {
extends ArrayBufferLikeFieldRuntimeValue<FixedLengthArrayBufferLikeFieldDefinition>{
public static getSize(descriptor: FixedLengthArrayBufferLikeFieldDefinition) {
return descriptor.options.length;
}
}
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.FixedLengthArrayBufferLike,
FixedLengthArrayBufferFieldRuntimeValue,
);

View file

@ -1,49 +1,49 @@
import { StructDefaultOptions, StructDeserializationContext } from '../runtime';
import { NumberFieldSubType, NumberFieldRuntimeValue } from './number';
import { StructDefaultOptions, StructDeserializationContext } from '../basic';
import { NumberFieldDefinition, NumberFieldRuntimeValue, NumberFieldType } from './number';
describe('Types', () => {
describe('Number', () => {
describe('NumberFieldSubType', () => {
describe('NumberFieldType', () => {
it('Uint8 validation', () => {
const key = 'Uint8';
expect(NumberFieldSubType[key]).toHaveProperty('size', 1);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key);
expect(NumberFieldType[key]).toHaveProperty('size', 1);
expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
});
it('Uint16 validation', () => {
const key = 'Uint16';
expect(NumberFieldSubType[key]).toHaveProperty('size', 2);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key);
expect(NumberFieldType[key]).toHaveProperty('size', 2);
expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
});
it('Int32 validation', () => {
const key = 'Int32';
expect(NumberFieldSubType[key]).toHaveProperty('size', 4);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key);
expect(NumberFieldType[key]).toHaveProperty('size', 4);
expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
});
it('Uint32 validation', () => {
const key = 'Uint32';
expect(NumberFieldSubType[key]).toHaveProperty('size', 4);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'set' + key);
expect(NumberFieldType[key]).toHaveProperty('size', 4);
expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'get' + key);
expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'set' + key);
});
it('Int64 validation', () => {
const key = 'Int64';
expect(NumberFieldSubType[key]).toHaveProperty('size', 8);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'getBig' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'setBig' + key);
expect(NumberFieldType[key]).toHaveProperty('size', 8);
expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'getBig' + key);
expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'setBig' + key);
});
it('Uint64 validation', () => {
const key = 'Uint64';
expect(NumberFieldSubType[key]).toHaveProperty('size', 8);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewGetter', 'getBig' + key);
expect(NumberFieldSubType[key]).toHaveProperty('dataViewSetter', 'setBig' + key);
expect(NumberFieldType[key]).toHaveProperty('size', 8);
expect(NumberFieldType[key]).toHaveProperty('dataViewGetter', 'getBig' + key);
expect(NumberFieldType[key]).toHaveProperty('dataViewSetter', 'setBig' + key);
});
});
@ -57,7 +57,7 @@ describe('Types', () => {
};
const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint8 } as any,
new NumberFieldDefinition(NumberFieldType.Uint8),
StructDefaultOptions,
undefined as any,
undefined as any,
@ -66,7 +66,7 @@ describe('Types', () => {
expect(value.get()).toBe(1);
expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint8.size);
expect(read).lastCalledWith(NumberFieldType.Uint8.size);
});
it('should deserialize Uint16', async () => {
@ -78,7 +78,7 @@ describe('Types', () => {
};
const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint16 } as any,
new NumberFieldDefinition(NumberFieldType.Uint16),
StructDefaultOptions,
undefined as any,
undefined as any,
@ -87,7 +87,7 @@ describe('Types', () => {
expect(value.get()).toBe((1 << 8) | 2);
expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint16.size);
expect(read).lastCalledWith(NumberFieldType.Uint16.size);
});
it('should deserialize Uint16LE', async () => {
@ -99,7 +99,7 @@ describe('Types', () => {
};
const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint16 } as any,
new NumberFieldDefinition(NumberFieldType.Uint16),
{ ...StructDefaultOptions, littleEndian: true },
undefined as any,
undefined as any,
@ -108,7 +108,7 @@ describe('Types', () => {
expect(value.get()).toBe((2 << 8) | 1);
expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint16.size);
expect(read).lastCalledWith(NumberFieldType.Uint16.size);
});
});
});

View file

@ -1,4 +1,4 @@
import { BuiltInFieldType, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, StructDeserializationContext } from '../runtime';
import { FieldDefinition, FieldRuntimeValue, StructDeserializationContext, StructOptions, StructSerializationContext } from '../basic';
export type DataViewGetters =
{ [TKey in keyof DataView]: TKey extends `get${string}` ? TKey : never }[keyof DataView];
@ -6,20 +6,20 @@ export type DataViewGetters =
export type DataViewSetters =
{ [TKey in keyof DataView]: TKey extends `set${string}` ? TKey : never }[keyof DataView];
export class NumberFieldSubType<TTypeScriptType extends number | bigint = number | bigint> {
public static readonly Uint8 = new NumberFieldSubType<number>(1, 'getUint8', 'setUint8');
export class NumberFieldType<TTypeScriptType extends number | bigint = number | bigint> {
public static readonly Uint8 = new NumberFieldType<number>(1, 'getUint8', 'setUint8');
public static readonly Uint16 = new NumberFieldSubType<number>(2, 'getUint16', 'setUint16');
public static readonly Uint16 = new NumberFieldType<number>(2, 'getUint16', 'setUint16');
public static readonly Int32 = new NumberFieldSubType<number>(4, 'getInt32', 'setInt32');
public static readonly Int32 = new NumberFieldType<number>(4, 'getInt32', 'setInt32');
public static readonly Uint32 = new NumberFieldSubType<number>(4, 'getUint32', 'setUint32');
public static readonly Uint32 = new NumberFieldType<number>(4, 'getUint32', 'setUint32');
public static readonly Int64 = new NumberFieldSubType<bigint>(8, 'getBigInt64', 'setBigInt64');
public static readonly Int64 = new NumberFieldType<bigint>(8, 'getBigInt64', 'setBigInt64');
public static readonly Uint64 = new NumberFieldSubType<bigint>(8, 'getBigUint64', 'setBigUint64');
public static readonly Uint64 = new NumberFieldType<bigint>(8, 'getBigUint64', 'setBigUint64');
public readonly value!: TTypeScriptType;
public readonly valueType!: TTypeScriptType;
public readonly size: number;
@ -38,33 +38,44 @@ export class NumberFieldSubType<TTypeScriptType extends number | bigint = number
}
}
export interface NumberFieldDescriptor<
TName extends string = string,
TSubType extends NumberFieldSubType<any> = NumberFieldSubType<any>,
TTypeScriptType = TSubType['value'],
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> extends FieldDescriptorBase<
TName,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions
export class NumberFieldDefinition<
TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = TType['valueType'],
> extends FieldDefinition<
void,
TTypeScriptType,
never
> {
type: BuiltInFieldType.Number;
public readonly type: TType;
subType: TSubType;
}
export class NumberFieldRuntimeValue extends FieldRuntimeValue<NumberFieldDescriptor> {
public static getSize(descriptor: NumberFieldDescriptor): number {
return descriptor.subType.size;
public constructor(type: TType, _typescriptType?: TTypeScriptType) {
super();
this.type = type;
}
public getSize(): number {
return this.type.size;
}
public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): NumberFieldRuntimeValue<TType, TTypeScriptType> {
return new NumberFieldRuntimeValue(this, options, context, object);
}
}
export class NumberFieldRuntimeValue<
TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = TType['valueType'],
> extends FieldRuntimeValue<NumberFieldDefinition<TType, TTypeScriptType>> {
protected value: number | bigint | undefined;
public async deserialize(context: StructDeserializationContext): Promise<void> {
const buffer = await context.read(this.getSize());
const view = new DataView(buffer);
this.value = view[this.descriptor.subType.dataViewGetter](
this.value = view[this.definition.type.dataViewGetter](
0,
this.options.littleEndian
);
@ -82,15 +93,10 @@ export class NumberFieldRuntimeValue extends FieldRuntimeValue<NumberFieldDescri
// `setBigInt64` requires a `bigint` while others require `number`
// So `dataView[DataViewSetters]` requires `bigint & number`
// and that is, `never`
(dataView[this.descriptor.subType.dataViewSetter] as any)(
(dataView[this.definition.type.dataViewSetter] as any)(
offset,
this.value!,
this.options.littleEndian
);
}
}
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.Number,
NumberFieldRuntimeValue,
);

View file

@ -1,47 +1,47 @@
import { BuiltInFieldType, FieldDescriptorBaseOptions, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructOptions, StructSerializationContext } from '../runtime';
import { ArrayBufferLikeFieldDescriptor, ArrayBufferLikeFieldRuntimeValue } from './array-buffer';
import { NumberFieldDescriptor, NumberFieldRuntimeValue } from './number';
import { getRuntimeValue, setRuntimeValue, StructOptions, StructSerializationContext } from '../basic';
import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldRuntimeValue, ArrayBufferLikeFieldType } from './array-buffer';
import { NumberFieldDefinition, NumberFieldRuntimeValue } from './number';
import { KeysOfType } from './utils';
export namespace VariableLengthArrayBufferFieldDescriptor {
export interface Options<
export interface VariableLengthArrayBufferLikeFieldOptions<
TInit = object,
TLengthField extends KeysOfType<TInit, number | string> = any,
> extends FieldDescriptorBaseOptions {
> {
lengthField: TLengthField;
}
export class VariableLengthArrayBufferLikeFieldDefinition<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TOptions extends VariableLengthArrayBufferLikeFieldOptions = VariableLengthArrayBufferLikeFieldOptions
> extends ArrayBufferLikeFieldDefinition<
TType,
TOptions,
TOptions['lengthField']
> {
public getSize(): number {
return 0;
}
public createValue(
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
): VariableLengthArrayBufferLikeFieldRuntimeValue {
return new VariableLengthArrayBufferLikeFieldRuntimeValue(this, options, context, object);
}
}
export interface VariableLengthArrayBufferFieldDescriptor<
TName extends string = string,
TType extends ArrayBufferLikeFieldDescriptor.SubType = ArrayBufferLikeFieldDescriptor.SubType,
TInit = object,
TLengthField extends KeysOfType<TInit, number | string> = any,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>,
TOptions extends VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField> = VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>
> extends ArrayBufferLikeFieldDescriptor<
TName,
TType,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType> & Record<TLengthField, never>,
TOptions
> {
type: BuiltInFieldType.VariableLengthArrayBufferLike;
options: TOptions;
}
class VariableLengthArrayBufferLengthFieldRuntimeValue extends NumberFieldRuntimeValue {
protected arrayBufferValue: VariableLengthArrayBufferFieldRuntimeValue;
class VariableLengthArrayBufferLikeLengthFieldRuntimeValue extends NumberFieldRuntimeValue {
protected arrayBufferValue: VariableLengthArrayBufferLikeFieldRuntimeValue;
public constructor(
descriptor: NumberFieldDescriptor,
definition: NumberFieldDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
arrayBufferValue: VariableLengthArrayBufferFieldRuntimeValue,
arrayBufferValue: VariableLengthArrayBufferLikeFieldRuntimeValue,
) {
super(descriptor, options, context, object);
super(definition, options, context, object);
this.arrayBufferValue = arrayBufferValue;
}
@ -61,28 +61,28 @@ class VariableLengthArrayBufferLengthFieldRuntimeValue extends NumberFieldRuntim
}
}
class VariableLengthArrayBufferFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<VariableLengthArrayBufferFieldDescriptor> {
class VariableLengthArrayBufferLikeFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<VariableLengthArrayBufferLikeFieldDefinition> {
public static getSize() {
return 0;
}
protected length: number | undefined;
protected lengthFieldValue: VariableLengthArrayBufferLengthFieldRuntimeValue;
protected lengthFieldValue: VariableLengthArrayBufferLikeLengthFieldRuntimeValue;
public constructor(
descriptor: VariableLengthArrayBufferFieldDescriptor,
descriptor: VariableLengthArrayBufferLikeFieldDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
) {
super(descriptor, options, context, object);
const lengthField = this.descriptor.options.lengthField;
const lengthField = this.definition.options.lengthField;
const oldValue = getRuntimeValue(object, lengthField) as NumberFieldRuntimeValue;
this.lengthFieldValue = new VariableLengthArrayBufferLengthFieldRuntimeValue(
oldValue.descriptor,
this.lengthFieldValue = new VariableLengthArrayBufferLikeLengthFieldRuntimeValue(
oldValue.definition,
this.options,
this.context,
object,
@ -98,19 +98,17 @@ class VariableLengthArrayBufferFieldRuntimeValue
public getSize() {
if (this.length === undefined) {
switch (this.descriptor.subType) {
case ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer:
this.length = this.arrayBuffer!.byteLength;
break;
case ArrayBufferLikeFieldDescriptor.SubType.Uint8ClampedArray:
this.length = this.typedArray!.byteLength;
break;
case ArrayBufferLikeFieldDescriptor.SubType.String:
this.arrayBuffer = this.context.encodeUtf8(this.string!);
if (this.arrayBuffer !== undefined) {
this.length = this.arrayBuffer.byteLength;
} else {
this.length = this.definition.type.getSize(this.typedValue);
if (this.length === -1) {
this.arrayBuffer = this.definition.type.toArrayBuffer(this.typedValue, this.context);
this.length = this.arrayBuffer.byteLength;
break;
}
}
}
return this.length;
}
@ -119,8 +117,3 @@ class VariableLengthArrayBufferFieldRuntimeValue
this.length = undefined;
}
}
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.VariableLengthArrayBufferLike,
VariableLengthArrayBufferFieldRuntimeValue,
);

View file

@ -3,7 +3,8 @@
"compilerOptions": {
"noEmit": true,
"types": [
"jest"
"@types/node",
"jest",
]
},
"exclude": []