refactor(struct): rework on type plugin system

This commit is contained in:
Simon Chan 2021-01-05 19:50:26 +08:00
parent 389157ac87
commit 8eed9aa3b7
51 changed files with 10921 additions and 951 deletions

6
.gitignore vendored
View file

@ -1,5 +1,7 @@
lib cjs
esm
dts
node_modules node_modules
*.log *.log
tsconfig.tsbuildinfo *.tsbuildinfo
/package-lock.json /package-lock.json

View file

@ -45,6 +45,7 @@
"sysui", "sysui",
"tcpip", "tcpip",
"tinyh", "tinyh",
"tsbuildinfo",
"uifabric", "uifabric",
"webadb", "webadb",
"websockify", "websockify",

View file

@ -9,7 +9,9 @@
"author": "Simon Chan <cnsimonchan@live.com>", "author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme", "homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "main": "cjs/index.js",
"module": "esm/index.js",
"types": "dts/index.d.ts",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git" "url": "git+https://github.com/yume-chan/ya-webadb.git"

View file

@ -1,13 +1,24 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "lib", // /* Redirect output structure to the directory. */ "outDir": "esm",
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "declarationDir": "dts",
"rootDir": "./src",
"lib": [ "lib": [
"ESNext", "ESNext",
"DOM" "DOM"
],
"types": [
"@types/w3c-web-usb",
"@yume-chan/adb"
] ]
}, },
"include": [
"src"
],
"exclude": [
"src/**/*.spec.ts"
],
"references": [ "references": [
{ {
"path": "../adb/tsconfig.json" "path": "../adb/tsconfig.json"

View file

@ -8,7 +8,9 @@
"author": "Simon Chan <cnsimonchan@live.com>", "author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme", "homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "main": "cjs/index.js",
"module": "esm/index.js",
"types": "dts/index.d.ts",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git" "url": "git+https://github.com/yume-chan/ya-webadb.git"

View file

@ -1,4 +1,4 @@
import { BackingField, Struct, StructInitType, StructValueType } from '@yume-chan/struct'; import { Struct, StructInitType, StructValueType } from '@yume-chan/struct';
import { AdbBackend } from './backend'; import { AdbBackend } from './backend';
import { BufferedStream } from './stream'; import { BufferedStream } from './stream';
@ -11,7 +11,7 @@ export enum AdbCommand {
Write = 0x45545257, // 'WRTE' Write = 0x45545257, // 'WRTE'
} }
const AdbPacketWithoutPayload = const AdbPacketHeader =
new Struct({ littleEndian: true }) new Struct({ littleEndian: true })
.uint32('command', undefined) .uint32('command', undefined)
.uint32('arg0') .uint32('arg0')
@ -21,13 +21,8 @@ const AdbPacketWithoutPayload =
.int32('magic'); .int32('magic');
const AdbPacketStruct = const AdbPacketStruct =
AdbPacketWithoutPayload AdbPacketHeader
.arrayBuffer('payload', { lengthField: 'payloadLength' }) .arrayBuffer('payload', { lengthField: 'payloadLength' });
.afterParsed((value) => {
if (value[BackingField].magic !== value.magic) {
throw new Error('Invalid command');
}
});
export type AdbPacket = StructValueType<typeof AdbPacketStruct>; export type AdbPacket = StructValueType<typeof AdbPacketStruct>;
@ -37,7 +32,7 @@ export namespace AdbPacket {
export function create( export function create(
init: AdbPacketInit, init: AdbPacketInit,
calculateChecksum: boolean, calculateChecksum: boolean,
backend: AdbBackend backend: AdbBackend,
): AdbPacket { ): AdbPacket {
let checksum: number; let checksum: number;
if (calculateChecksum && init.payload) { if (calculateChecksum && init.payload) {
@ -85,8 +80,9 @@ export namespace AdbPacket {
export async function write(packet: AdbPacket, backend: AdbBackend): Promise<void> { export async function write(packet: AdbPacket, backend: AdbBackend): Promise<void> {
// Write payload separately to avoid an extra copy // Write payload separately to avoid an extra copy
await backend.write(AdbPacketWithoutPayload.serialize(packet, backend)); const header = AdbPacketHeader.serialize(packet, backend);
if (packet.payload) { await backend.write(header);
if (packet.payload.byteLength) {
await backend.write(packet.payload); await backend.write(packet.payload);
} }
} }

View file

@ -23,6 +23,8 @@ export interface AdbIncomingSocketEventArgs {
socket: AdbSocket; socket: AdbSocket;
} }
const EmptyArrayBuffer = new ArrayBuffer(0);
export class AdbPacketDispatcher extends AutoDisposable { export class AdbPacketDispatcher extends AutoDisposable {
// ADB socket id starts from 1 // ADB socket id starts from 1
// (0 means open failed) // (0 means open failed)
@ -222,10 +224,10 @@ export class AdbPacketDispatcher extends AutoDisposable {
packetOrCommand: AdbPacketInit | AdbCommand, packetOrCommand: AdbPacketInit | AdbCommand,
arg0?: number, arg0?: number,
arg1?: number, arg1?: number,
payload?: string | ArrayBuffer payload: string | ArrayBuffer = EmptyArrayBuffer,
): Promise<void> { ): Promise<void> {
let init: AdbPacketInit; let init: AdbPacketInit;
if (arguments.length === 1) { if (arg0 === undefined) {
init = packetOrCommand as AdbPacketInit; init = packetOrCommand as AdbPacketInit;
} else { } else {
init = { init = {

View file

@ -1,15 +1,23 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "lib", // /* Redirect output structure to the directory. */ "target": "ES2016",
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "outDir": "esm",
"declarationDir": "dts",
"rootDir": "./src"
}, },
"include": [
"src"
],
"exclude": [
"src/**/*.spec.ts"
],
"references": [ "references": [
{ {
"path": "../event/tsconfig.json" "path": "../event/tsconfig.esm.json"
}, },
{ {
"path": "../struct/tsconfig.json" "path": "../struct/tsconfig.esm.json"
} }
] ]
} }

View file

@ -1,4 +1,4 @@
import { Icon, MessageBar, StackItem, TooltipHost, Text, Separator } from '@fluentui/react'; import { Icon, MessageBar, Separator, TooltipHost } from '@fluentui/react';
import { AdbFeatures } from '@yume-chan/adb'; import { AdbFeatures } from '@yume-chan/adb';
import React from 'react'; import React from 'react';
import { ExternalLink } from '../components'; import { ExternalLink } from '../components';

View file

@ -2,15 +2,32 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"target": "ES2016",
"lib": [ "lib": [
"ESNext", "ESNext",
"DOM" "DOM"
], ],
"jsx": "react", // /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "jsx": "react", // /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": false, // /* Generates corresponding '.d.ts' file. */
"declarationMap": false, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
"composite": false, // /* Enable project compilation */ "composite": false, // /* Enable project compilation */
"types": [
"@types/node",
"@types/react",
"@types/react-dom",
"@types/react-router-dom",
"@fluentui/react",
"@yume-chan/adb",
"@yume-chan/adb-backend-web",
"@yume-chan/async-operation-manager",
"@yume-chan/event",
"@yume-chan/struct"
]
}, },
"include": [
"src"
],
"exclude": [
"src/**/*.spec.ts"
],
"references": [ "references": [
{ {
"path": "../adb-backend-web/tsconfig.json" "path": "../adb-backend-web/tsconfig.json"

File diff suppressed because it is too large Load diff

View file

@ -5,25 +5,39 @@
"keywords": [ "keywords": [
"event" "event"
], ],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "author": {
"name": "Simon Chan",
"email": "cnsimonchan@live.com",
"url": "https://chensi.moe/blog"
},
"homepage": "https://github.com/yume-chan/ya-webadb/tree/master/packages/event#readme",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git" "url": "git+https://github.com/yume-chan/ya-webadb.git",
}, "directory": "packages/event"
"scripts": {
"build": "tsc -b",
"build:watch": "tsc -b -w"
}, },
"bugs": { "bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues" "url": "https://github.com/yume-chan/ya-webadb/issues"
}, },
"devDependencies": { "main": "cjs/index.js",
"typescript": "4.1.3" "module": "esm/index.js",
"types": "dts/index.d.ts",
"scripts": {
"build": "rimraf {cjs,esm,dts,*.tsbuildinfo} && tsc -b tsconfig.esm.json tsconfig.cjs.json",
"build:watch": "tsc -b -w tsconfig.esm.json tsconfig.cjs.json",
"test": "jest",
"coverage": "jest --coverage",
"prepublishOnly": "npm run build"
}, },
"dependencies": { "dependencies": {
"tslib": "2.0.3" "tslib": "2.0.3"
},
"devDependencies": {
"@types/jest": "26.0.19",
"jest": "26.6.3",
"rimraf": "3.0.2",
"ts-jest": "26.4.4",
"typescript": "4.1.3"
} }
} }

View file

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.esm.json",
"compilerOptions": {
"composite": false,
"module": "CommonJS",
"outDir": "cjs",
"declaration": false,
"declarationDir": null,
"declarationMap": false
}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "esm",
"declarationDir": "dts",
"types": [],
},
"include": [
"src"
],
"exclude": [
"src/**/*.spec.ts"
]
}

View file

@ -1,7 +1,10 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "./tsconfig.esm.json",
"compilerOptions": { "compilerOptions": {
"outDir": "lib", // /* Redirect output structure to the directory. */ "noEmit": true,
"rootDir": "./src" // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "types": [
} "jest"
]
},
"exclude": []
} }

View file

@ -0,0 +1,10 @@
.github
.vscode
coverage
src/**.spec.ts
jest.config.js
pnpm-lock.yaml
renovate.json
tsconfig.json
*.tsbuildinfo

View file

@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

File diff suppressed because it is too large Load diff

View file

@ -6,25 +6,39 @@
"structure", "structure",
"typescript" "typescript"
], ],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT", "license": "MIT",
"main": "lib/index.js", "author": {
"name": "Simon Chan",
"email": "cnsimonchan@live.com",
"url": "https://chensi.moe/blog"
},
"homepage": "https://github.com/yume-chan/ya-webadb/tree/master/packages/struct#readme",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git" "url": "git+https://github.com/yume-chan/ya-webadb.git",
}, "directory": "packages/struct"
"scripts": {
"build": "tsc -b",
"build:watch": "tsc -b -w"
}, },
"bugs": { "bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues" "url": "https://github.com/yume-chan/ya-webadb/issues"
}, },
"devDependencies": { "main": "cjs/index.js",
"typescript": "4.1.3" "module": "esm/index.js",
"types": "dts/index.d.ts",
"scripts": {
"build": "rimraf {cjs,esm,dts,*.tsbuildinfo} && tsc -b tsconfig.esm.json tsconfig.cjs.json",
"build:watch": "tsc -b -w tsconfig.esm.json tsconfig.cjs.json",
"test": "jest",
"coverage": "jest --coverage",
"prepublishOnly": "npm run build"
}, },
"dependencies": { "dependencies": {
"tslib": "2.0.3" "tslib": "2.0.3"
},
"devDependencies": {
"@types/jest": "26.0.19",
"jest": "26.6.3",
"rimraf": "3.0.2",
"ts-jest": "26.4.4",
"typescript": "4.1.3"
} }
} }

View file

@ -1,20 +0,0 @@
export const BackingField = Symbol('BackingField');
export function getBackingField<T = unknown>(object: unknown, field: string): T {
return (object as any)[BackingField][field] as T;
}
export function setBackingField(object: unknown, field: string, value: any): void {
(object as any)[BackingField][field] = value;
}
export function defineSimpleAccessors(object: unknown, field: string): void {
Object.defineProperty(object, field, {
configurable: true,
enumerable: true,
get() { return getBackingField(object, field); },
set(value) { setBackingField(object, field, value); },
});
}
export type WithBackingField<T> = T & { [BackingField]: any; };

View file

@ -1,67 +0,0 @@
import { getBackingField, setBackingField } from '../backing-field';
import { FieldDescriptorBase, FieldDescriptorBaseOptions } from './descriptor';
export namespace Array {
export enum SubType {
ArrayBuffer,
String,
}
export type TypeScriptType<TType extends SubType = SubType> =
TType extends SubType.ArrayBuffer ? ArrayBuffer :
TType extends SubType.String ? string :
ArrayBuffer | string;
export interface BackingField {
buffer?: ArrayBuffer;
string?: string;
}
export function initialize(object: any, field: Array, value: BackingField): void {
switch (field.subType) {
case SubType.ArrayBuffer:
Object.defineProperty(object, field.name, {
configurable: true,
enumerable: true,
get(): ArrayBuffer {
return getBackingField<BackingField>(object, field.name).buffer!;
},
set(buffer: ArrayBuffer) {
setBackingField(object, field.name, { buffer });
},
});
break;
case SubType.String:
Object.defineProperty(object, field.name, {
configurable: true,
enumerable: true,
get(): string {
return getBackingField<BackingField>(object, field.name).string!;
},
set(string: string) {
setBackingField(object, field.name, { string });
},
});
break;
default:
throw new Error('Unknown type');
}
setBackingField(object, field.name, value);
}
}
export interface Array<
TName extends string = string,
TType extends Array.SubType = Array.SubType,
TResultObject = {},
TInitObject = {},
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> extends FieldDescriptorBase<
TName,
TResultObject,
TInitObject,
TOptions
> {
subType: TType;
}

View file

@ -1,64 +0,0 @@
import { StructDeserializationContext, StructOptions, StructSerializationContext } from '../types';
import { FieldDescriptorBase, FieldType } from './descriptor';
export interface FieldTypeDefinition<
TDescriptor extends FieldDescriptorBase = FieldDescriptorBase,
TInitExtra = undefined,
> {
type: FieldType | string;
deserialize(options: {
context: StructDeserializationContext;
field: TDescriptor;
object: any;
options: StructOptions;
}): Promise<{ value: any; extra?: TInitExtra; }>;
getSize(options: {
field: TDescriptor;
options: StructOptions;
}): number;
getDynamicSize?(options: {
context: StructSerializationContext;
field: TDescriptor;
object: any;
options: StructOptions;
}): number;
initialize?(options: {
context: StructSerializationContext;
field: TDescriptor;
value: any;
extra?: TInitExtra;
object: any;
options: StructOptions;
}): void;
serialize(options: {
context: StructSerializationContext;
dataView: DataView;
field: TDescriptor;
object: any;
offset: number;
options: StructOptions;
}): void;
}
const registry: Record<number | string, FieldTypeDefinition<any, any>> = {};
export function getFieldTypeDefinition(type: FieldType | string): FieldTypeDefinition<any, any> {
return registry[type];
}
export function registerFieldTypeDefinition<
TDescriptor extends FieldDescriptorBase,
TInitExtra,
TDefinition extends FieldTypeDefinition<TDescriptor, TInitExtra>
>(
_field: TDescriptor,
_initExtra: TInitExtra,
methods: TDefinition
): void {
registry[methods.type] = methods;
}

View file

@ -1,82 +0,0 @@
import { getBackingField } from '../backing-field';
import { placeholder } from '../utils';
import { Array } from './array';
import { registerFieldTypeDefinition } from './definition';
import { FieldDescriptorBaseOptions, FieldType } from './descriptor';
export namespace FixedLengthArray {
export interface Options extends FieldDescriptorBaseOptions {
length: number;
}
}
export interface FixedLengthArray<
TName extends string = string,
TType extends Array.SubType = Array.SubType,
TTypeScriptType = Array.TypeScriptType<TType>,
TOptions extends FixedLengthArray.Options = FixedLengthArray.Options
> extends Array<
TName,
TType,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions
> {
type: FieldType.FixedLengthArray;
options: TOptions;
};
registerFieldTypeDefinition(
placeholder<FixedLengthArray>(),
placeholder<ArrayBuffer>(),
{
type: FieldType.FixedLengthArray,
async deserialize(
{ context, field }
): Promise<{ value: string | ArrayBuffer, extra?: ArrayBuffer; }> {
const buffer = await context.read(field.options.length);
switch (field.subType) {
case Array.SubType.ArrayBuffer:
return { value: buffer };
case Array.SubType.String:
return {
value: context.decodeUtf8(buffer),
extra: buffer
};
default:
throw new Error('Unknown type');
}
},
getSize({ field }) {
return field.options.length;
},
initialize({ extra, field, object, value }) {
const backingField: Array.BackingField = {};
if (typeof value === 'string') {
backingField.string = value;
if (extra) {
backingField.buffer = extra;
}
} else {
backingField.buffer = value;
}
Array.initialize(object, field, backingField);
},
serialize({ context, dataView, field, object, offset }) {
const backingField = getBackingField<Array.BackingField>(object, field.name);
backingField.buffer ??=
context.encodeUtf8(backingField.string!);
new Uint8Array(dataView.buffer).set(
new Uint8Array(backingField.buffer),
offset
);
}
}
);

View file

@ -1,6 +0,0 @@
export * from './array';
export * from './definition';
export * from './descriptor';
export * from './fixed-length-array';
export * from './number';
export * from './variable-length-array';

View file

@ -1,92 +0,0 @@
import { placeholder } from '../utils';
import { registerFieldTypeDefinition } from './definition';
import { FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType } from './descriptor';
export namespace Number {
export type TypeScriptType<T extends SubType> =
T extends SubType.Uint64 ? bigint :
T extends SubType.Int64 ? bigint :
number;
export enum SubType {
Uint8,
Uint16,
Int32,
Uint32,
Uint64,
Int64,
}
export const SizeMap: Record<SubType, number> = {
[SubType.Uint8]: 1,
[SubType.Uint16]: 2,
[SubType.Int32]: 4,
[SubType.Uint32]: 4,
[SubType.Uint64]: 8,
[SubType.Int64]: 8,
};
export const DataViewGetterMap = {
[SubType.Uint8]: 'getUint8',
[SubType.Uint16]: 'getUint16',
[SubType.Int32]: 'getInt32',
[SubType.Uint32]: 'getUint32',
[SubType.Uint64]: 'getBigUint64',
[SubType.Int64]: 'getBigInt64',
} as const;
export const DataViewSetterMap = {
[SubType.Uint8]: 'setUint8',
[SubType.Uint16]: 'setUint16',
[SubType.Int32]: 'setInt32',
[SubType.Uint32]: 'setUint32',
[SubType.Uint64]: 'setBigUint64',
[SubType.Int64]: 'setBigInt64',
} as const;
}
export interface Number<
TName extends string = string,
TSubType extends Number.SubType = Number.SubType,
TTypeScriptType = Number.TypeScriptType<TSubType>,
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> extends FieldDescriptorBase<
TName,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions
> {
type: FieldType.Number;
subType: TSubType;
}
registerFieldTypeDefinition(
placeholder<Number>(),
undefined,
{
type: FieldType.Number,
getSize({ field }) {
return Number.SizeMap[field.subType];
},
async deserialize({ context, field, options }) {
const buffer = await context.read(Number.SizeMap[field.subType]);
const view = new DataView(buffer);
const value = view[Number.DataViewGetterMap[field.subType]](
0,
options.littleEndian
);
return { value };
},
serialize({ dataView, field, object, offset, options }) {
(dataView[Number.DataViewSetterMap[field.subType]] as any)(
offset,
object[field.name],
options.littleEndian
);
},
}
);

View file

@ -1,221 +0,0 @@
import { getBackingField, setBackingField } from '../backing-field';
import { StructSerializationContext } from '../types';
import { Identity, placeholder } from '../utils';
import { Array } from './array';
import { registerFieldTypeDefinition } from './definition';
import { FieldDescriptorBaseOptions, FieldType } from './descriptor';
export namespace VariableLengthArray {
export type TypeScriptTypeCanBeUndefined<
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> =
TEmptyBehavior extends EmptyBehavior.Empty ? never :
undefined;
export type TypeScriptType<
TType extends Array.SubType = Array.SubType,
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> =
Identity<
Array.TypeScriptType<TType> |
TypeScriptTypeCanBeUndefined<TEmptyBehavior>
>;
export enum EmptyBehavior {
Undefined,
Empty,
}
export type KeyOfType<TObject, TProperty> =
{
[TKey in keyof TObject]:
TObject[TKey] extends TProperty ? TKey : never
}[keyof TObject];
export interface Options<
TInit = object,
TLengthField extends KeyOfType<TInit, number | string> = any,
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> extends FieldDescriptorBaseOptions {
lengthField: TLengthField;
emptyBehavior?: TEmptyBehavior;
}
export function getLengthBackingField(
object: any,
field: VariableLengthArray
): number | undefined {
return getBackingField<number>(object, field.options.lengthField);
}
export function setLengthBackingField(
object: any,
field: VariableLengthArray,
value: number | undefined
) {
setBackingField(object, field.options.lengthField, value);
}
export function initialize(
object: any,
field: VariableLengthArray,
value: Array.BackingField,
context: StructSerializationContext,
): void {
Array.initialize(object, field, value);
const descriptor = Object.getOwnPropertyDescriptor(object, field.name)!;
delete object[field.name];
switch (field.subType) {
case Array.SubType.ArrayBuffer:
Object.defineProperty(object, field.name, {
...descriptor,
set(buffer: ArrayBuffer | undefined) {
descriptor.set!.call(object, buffer);
setLengthBackingField(object, field, buffer?.byteLength ?? 0);
},
});
delete object[field.options.lengthField];
Object.defineProperty(object, field.options.lengthField, {
configurable: true,
enumerable: true,
get() {
return getLengthBackingField(object, field);
}
});
break;
case Array.SubType.String:
Object.defineProperty(object, field.name, {
...descriptor,
set(string: string | undefined) {
descriptor.set!.call(object, string);
if (string) {
setLengthBackingField(object, field, undefined);
} else {
setLengthBackingField(object, field, 0);
}
},
});
delete object[field.options.lengthField];
Object.defineProperty(object, field.options.lengthField, {
configurable: true,
enumerable: true,
get() {
let value = getLengthBackingField(object, field);
if (value === undefined) {
const backingField = getBackingField<Array.BackingField>(object, field.name);
const buffer = context.encodeUtf8(backingField.string!);
backingField.buffer = buffer;
value = buffer.byteLength;
setLengthBackingField(object, field, value);
}
return value;
}
});
break;
default:
throw new Error('Unknown type');
}
setBackingField(object, field.name, value);
if (value.buffer) {
setLengthBackingField(object, field, value.buffer.byteLength);
}
}
}
export interface VariableLengthArray<
TName extends string = string,
TType extends Array.SubType = Array.SubType,
TInit = object,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string> = any,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior = VariableLengthArray.EmptyBehavior,
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>,
TOptions extends VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior> = VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>
> extends Array<
TName,
TType,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType> & Record<TLengthField, never>,
TOptions
> {
type: FieldType.VariableLengthArray;
options: TOptions;
}
registerFieldTypeDefinition(
placeholder<VariableLengthArray>(),
placeholder<ArrayBuffer>(),
{
type: FieldType.VariableLengthArray,
async deserialize(
{ context, field, object }
): Promise<{ value: string | ArrayBuffer | undefined, extra?: ArrayBuffer; }> {
let length = object[field.options.lengthField];
if (typeof length === 'string') {
length = Number.parseInt(length, 10);
}
if (length === 0) {
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
switch (field.subType) {
case Array.SubType.ArrayBuffer:
return { value: new ArrayBuffer(0) };
case Array.SubType.String:
return { value: '', extra: new ArrayBuffer(0) };
default:
throw new Error('Unknown type');
}
} else {
return { value: undefined };
}
}
const buffer = await context.read(length);
switch (field.subType) {
case Array.SubType.ArrayBuffer:
return { value: buffer };
case Array.SubType.String:
return {
value: context.decodeUtf8(buffer),
extra: buffer
};
default:
throw new Error('Unknown type');
}
},
getSize() { return 0; },
getDynamicSize({ field, object }) {
return object[field.options.lengthField];
},
initialize({ context, extra, field, object, value }) {
const backingField: Array.BackingField = {};
if (typeof value === 'string') {
backingField.string = value;
if (extra) {
backingField.buffer = extra;
}
} else {
backingField.buffer = value;
}
Array.initialize(object, field, backingField);
VariableLengthArray.initialize(object, field, backingField, context);
},
serialize({ dataView, field, object, offset }) {
const backingField = getBackingField<Array.BackingField>(object, field.name);
new Uint8Array(dataView.buffer).set(
new Uint8Array(backingField.buffer!),
offset
);
},
}
);

View file

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

View file

@ -0,0 +1,14 @@
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);
});
});
});

View file

@ -0,0 +1,45 @@
import type { StructFieldRuntimeTypeRegistry } from './registry';
import { GlobalStructFieldRuntimeTypeRegistry } from './registry';
/**
* Context with enough methods to serialize a struct
*/
export interface StructSerializationContext {
/**
* Encode the specified string into an ArrayBuffer using UTF-8 encoding
*/
encodeUtf8(input: string): ArrayBuffer;
}
/**
* Context with enough methods to deserialize a struct
*/
export interface StructDeserializationContext extends StructSerializationContext {
/**
* Decode the specified `ArrayBuffer` using UTF-8 encoding
*/
decodeUtf8(buffer: ArrayBuffer): string;
/**
* Read exactly `length` bytes of data from underlying storage.
*
* Errors can be thrown to indicates end of file or other errors.
*/
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
}
export interface StructOptions {
/**
* Whether multi-byte fields in this struct are in little endian
*
* Default to `false`
*/
littleEndian: boolean;
fieldRuntimeTypeRegistry: StructFieldRuntimeTypeRegistry;
}
export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false,
fieldRuntimeTypeRegistry: GlobalStructFieldRuntimeTypeRegistry,
};

View file

@ -1,7 +1,7 @@
export enum FieldType { export enum BuiltInFieldType {
Number, Number,
FixedLengthArray, FixedLengthArrayBufferLike,
VariableLengthArray, VariableLengthArrayBufferLike,
} }
export interface FieldDescriptorBaseOptions { export interface FieldDescriptorBaseOptions {
@ -14,7 +14,7 @@ export interface FieldDescriptorBase<
TInitObject = {}, TInitObject = {},
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
> { > {
type: FieldType | string; type: BuiltInFieldType | string;
name: TName; name: TName;

View file

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

View file

@ -0,0 +1,61 @@
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

@ -0,0 +1,26 @@
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,27 @@
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

@ -0,0 +1,45 @@
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

@ -0,0 +1,34 @@
import { createObjectWithRuntimeValues, getRuntimeValue, setRuntimeValue } from './runtime-value';
describe('Runtime', () => {
describe('RuntimeValue', () => {
it('`createObjectWithRuntimeValues` should create an object with symbol', () => {
const object = createObjectWithRuntimeValues();
expect(Object.getOwnPropertySymbols(object)).toHaveLength(1);
});
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);
});
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);
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,28 @@
import { FieldRuntimeValue } from './runtime-type';
const RuntimeValues = Symbol('RuntimeValues');
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); },
});
}

View file

@ -1,135 +1,166 @@
import { BackingField, defineSimpleAccessors, setBackingField, WithBackingField } from './backing-field'; import { BuiltInFieldType, createObjectWithRuntimeValues, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './runtime';
import { Array, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FieldTypeDefinition, FixedLengthArray, getFieldTypeDefinition, Number, VariableLengthArray } from './field'; import { ArrayBufferLikeFieldDescriptor, Evaluate, FixedLengthArrayBufferFieldDescriptor, Identity, KeysOfType, NumberFieldDescriptor, NumberFieldSubType, OmitNever, Overwrite, VariableLengthArrayBufferFieldDescriptor } from './types';
import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './types';
import { Evaluate, Identity, OmitNever, Overwrite } from './utils';
/**
* Extract the value type of the specified `Struct`
*
* The lack of generic constraint is on purpose to allow `StructLike` types
*/
export type StructValueType<T> = export type StructValueType<T> =
T extends { deserialize(context: StructDeserializationContext): Promise<infer R>; } ? R : never; T extends { deserialize(context: StructDeserializationContext): Promise<infer R>; } ? R : never;
/**
* Extract the init type of the specified `Struct`
*/
export type StructInitType<T extends Struct<object, object, object, unknown>> = export type StructInitType<T extends Struct<object, object, object, unknown>> =
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never; T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
interface AddArrayFieldDescriptor< /**
TResult extends object, * Create a new `Struct` type with `TDescriptor` appended
TInit extends object, */
TExtra extends object, type AddFieldDescriptor<
TAfterParsed TValue extends object,
> {
<
TName extends string,
TType extends Array.SubType,
TTypeScriptType = Array.TypeScriptType<TType>
>(
name: TName,
type: TType,
options: FixedLengthArray.Options,
typescriptType?: () => TTypeScriptType,
): MergeStruct<
TResult,
TInit,
TExtra,
TAfterParsed,
FixedLengthArray<
TName,
TType,
TTypeScriptType
>
>;
<
TName extends string,
TType extends Array.SubType,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
>(
name: TName,
type: TType,
options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
typescriptType?: () => TTypeScriptType,
): MergeStruct<
TResult,
TInit,
TExtra,
TAfterParsed,
VariableLengthArray<
TName,
TType,
TInit,
TLengthField,
TEmptyBehavior,
TTypeScriptType
>
>;
}
interface AddArraySubTypeFieldDescriptor<
TResult extends object,
TInit extends object, TInit extends object,
TExtra extends object, TExtra extends object,
TAfterParsed, TAfterParsed,
TType extends Array.SubType TDescriptor extends FieldDescriptorBase> =
> {
<
TName extends string,
TTypeScriptType = Array.TypeScriptType<TType>
>(
name: TName,
options: FixedLengthArray.Options,
typescriptType?: () => TTypeScriptType,
): MergeStruct<
TResult,
TInit,
TExtra,
TAfterParsed,
FixedLengthArray<
TName,
TType,
TTypeScriptType
>
>;
<
TName extends string,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
>(
name: TName,
options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
_typescriptType?: TTypeScriptType,
): MergeStruct<
TResult,
TInit,
TExtra,
TAfterParsed,
VariableLengthArray<
TName,
TType,
TInit,
TLengthField,
TEmptyBehavior,
TTypeScriptType
>
>;
}
type MergeStruct<
TResult extends object,
TInit extends object,
TExtra extends object,
TAfterParsed,
TDescriptor extends FieldDescriptorBase
> =
Identity<Struct< Identity<Struct<
Evaluate<TResult & Exclude<TDescriptor['resultObject'], undefined>>, // Merge two types
OmitNever<TInit & Exclude<TDescriptor['initObject'], undefined>>, 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>
>,
TExtra, TExtra,
TAfterParsed TAfterParsed
>>; >>;
/**
* Overload methods to add an array typed field
*/
interface AddArrayBufferFieldDescriptor<
TValue extends object,
TInit extends object,
TExtra extends object,
TAfterParsed
> {
/**
* Append a fixed-length array to the `Struct`
*
* @param name Name of the field
* @param type `Array.SubType.ArrayBuffer` or `Array.SubType.String`
* @param options Fixed-length array options
* @param typescriptType Type of the field in TypeScript.
* 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>
>(
name: TName,
type: TType,
options: FixedLengthArrayBufferFieldDescriptor.Options,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TValue,
TInit,
TExtra,
TAfterParsed,
FixedLengthArrayBufferFieldDescriptor<
TName,
TType,
TTypeScriptType
>
>;
/**
* 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>
>(
name: TName,
type: TType,
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TValue,
TInit,
TExtra,
TAfterParsed,
VariableLengthArrayBufferFieldDescriptor<
TName,
TType,
TInit,
TLengthField,
TTypeScriptType
>
>;
}
interface AddArrayBufferSubTypeFieldDescriptor<
TResult extends object,
TInit extends object,
TExtra extends object,
TAfterParsed,
TType extends ArrayBufferLikeFieldDescriptor.SubType
> {
<
TName extends string,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
>(
name: TName,
options: FixedLengthArrayBufferFieldDescriptor.Options,
typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TResult,
TInit,
TExtra,
TAfterParsed,
FixedLengthArrayBufferFieldDescriptor<
TName,
TType,
TTypeScriptType
>
>;
<
TName extends string,
TLengthField extends KeysOfType<TInit, number | string>,
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
>(
name: TName,
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>,
_typescriptType?: TTypeScriptType,
): AddFieldDescriptor<
TResult,
TInit,
TExtra,
TAfterParsed,
VariableLengthArrayBufferFieldDescriptor<
TName,
TType,
TInit,
TLengthField,
TTypeScriptType
>
>;
}
export type StructAfterParsed<TResult, TAfterParsed> = export type StructAfterParsed<TResult, TAfterParsed> =
(this: WithBackingField<TResult>, object: WithBackingField<TResult>) => TAfterParsed; (this: TResult, object: TResult) => TAfterParsed;
export default class Struct< export default class Struct<
TResult extends object = {}, TResult extends object = {},
@ -142,19 +173,19 @@ export default class Struct<
private _size = 0; private _size = 0;
public get size() { return this._size; } public get size() { return this._size; }
private fields: FieldDescriptorBase[] = []; private fieldDescriptors: FieldDescriptorBase[] = [];
private _extra: PropertyDescriptorMap = {}; private _extra: PropertyDescriptorMap = {};
private _afterParsed?: StructAfterParsed<any, any>; private _afterParsed?: StructAfterParsed<any, any>;
public constructor(options: Partial<StructOptions> = StructDefaultOptions) { public constructor(options?: Partial<StructOptions>) {
this.options = { ...StructDefaultOptions, ...options }; this.options = { ...StructDefaultOptions, ...options };
} }
private clone(): Struct<any, any, any, any> { private clone(): Struct<any, any, any, any> {
const result = new Struct<any, any, any, any>(this.options); const result = new Struct<any, any, any, any>(this.options);
result.fields = this.fields.slice(); result.fieldDescriptors = this.fieldDescriptors.slice();
result._size = this._size; result._size = this._size;
result._extra = this._extra; result._extra = this._extra;
result._afterParsed = this._afterParsed; result._afterParsed = this._afterParsed;
@ -162,13 +193,13 @@ export default class Struct<
} }
public field<TDescriptor extends FieldDescriptorBase>( public field<TDescriptor extends FieldDescriptorBase>(
field: TDescriptor, descriptor: TDescriptor,
): MergeStruct<TResult, TInit, TExtra, TAfterParsed, TDescriptor> { ): AddFieldDescriptor<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
const result = this.clone(); const result = this.clone();
result.fields.push(field); result.fieldDescriptors.push(descriptor);
const definition = getFieldTypeDefinition(field.type); const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
const size = definition.getSize({ field, options: this.options }); const size = Constructor.getSize(descriptor, this.options);
result._size += size; result._size += size;
return result; return result;
@ -176,16 +207,16 @@ export default class Struct<
private number< private number<
TName extends string, TName extends string,
TSubType extends Number.SubType = Number.SubType, TType extends NumberFieldSubType = NumberFieldSubType,
TTypeScriptType = Number.TypeScriptType<TSubType> TTypeScriptType = TType['value']
>( >(
name: TName, name: TName,
type: TSubType, type: TType,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType, _typescriptType?: TTypeScriptType,
) { ) {
return this.field<Number<TName, TSubType, TTypeScriptType>>({ return this.field<NumberFieldDescriptor<TName, TType, TTypeScriptType>>({
type: FieldType.Number, type: BuiltInFieldType.Number,
name, name,
subType: type, subType: type,
options, options,
@ -194,7 +225,7 @@ export default class Struct<
public uint8< public uint8<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint8> TTypeScriptType = (typeof NumberFieldSubType)['Uint8']['value']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
@ -202,7 +233,7 @@ export default class Struct<
) { ) {
return this.number( return this.number(
name, name,
Number.SubType.Uint8, NumberFieldSubType.Uint8,
options, options,
_typescriptType _typescriptType
); );
@ -210,7 +241,7 @@ export default class Struct<
public uint16< public uint16<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint16> TTypeScriptType = (typeof NumberFieldSubType)['Uint16']['value']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
@ -218,7 +249,7 @@ export default class Struct<
) { ) {
return this.number( return this.number(
name, name,
Number.SubType.Uint16, NumberFieldSubType.Uint16,
options, options,
_typescriptType _typescriptType
); );
@ -226,7 +257,7 @@ export default class Struct<
public int32< public int32<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Int32> TTypeScriptType = (typeof NumberFieldSubType)['Int32']['value']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
@ -234,7 +265,7 @@ export default class Struct<
) { ) {
return this.number( return this.number(
name, name,
Number.SubType.Int32, NumberFieldSubType.Int32,
options, options,
_typescriptType _typescriptType
); );
@ -242,7 +273,23 @@ export default class Struct<
public uint32< public uint32<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint32> TTypeScriptType = (typeof NumberFieldSubType)['Uint32']['value']
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
typescriptType?: TTypeScriptType,
) {
return this.number(
name,
NumberFieldSubType.Uint32,
options,
typescriptType
);
}
public int64<
TName extends string,
TTypeScriptType = (typeof NumberFieldSubType)['Int64']['value']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
@ -250,7 +297,7 @@ export default class Struct<
) { ) {
return this.number( return this.number(
name, name,
Number.SubType.Uint32, NumberFieldSubType.Int64,
options, options,
_typescriptType _typescriptType
); );
@ -258,7 +305,7 @@ export default class Struct<
public uint64< public uint64<
TName extends string, TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint64> TTypeScriptType = (typeof NumberFieldSubType)['Uint64']['value']
>( >(
name: TName, name: TName,
options: FieldDescriptorBaseOptions = {}, options: FieldDescriptorBaseOptions = {},
@ -266,43 +313,27 @@ export default class Struct<
) { ) {
return this.number( return this.number(
name, name,
Number.SubType.Uint64, NumberFieldSubType.Uint64,
options, options,
_typescriptType _typescriptType
); );
} }
public int64< private arrayBufferLike: AddArrayBufferFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
TName extends string,
TTypeScriptType = Number.TypeScriptType<Number.SubType.Int64>
>(
name: TName,
options: FieldDescriptorBaseOptions = {},
_typescriptType?: TTypeScriptType,
) {
return this.number(
name,
Number.SubType.Int64,
options,
_typescriptType
);
}
private array: AddArrayFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
name: string, name: string,
type: Array.SubType, type: ArrayBufferLikeFieldDescriptor.SubType,
options: FixedLengthArray.Options | VariableLengthArray.Options options: FixedLengthArrayBufferFieldDescriptor.Options | VariableLengthArrayBufferFieldDescriptor.Options
): Struct<any, any, any, any> => { ): Struct<any, any, any, any> => {
if ('length' in options) { if ('length' in options) {
return this.field<FixedLengthArray>({ return this.field<FixedLengthArrayBufferFieldDescriptor>({
type: FieldType.FixedLengthArray, type: BuiltInFieldType.FixedLengthArrayBufferLike,
name, name,
subType: type, subType: type,
options: options, options: options,
}); });
} else { } else {
return this.field<VariableLengthArray>({ return this.field<VariableLengthArrayBufferFieldDescriptor>({
type: FieldType.VariableLengthArray, type: BuiltInFieldType.VariableLengthArrayBufferLike,
name, name,
subType: type, subType: type,
options: options, options: options,
@ -310,30 +341,30 @@ export default class Struct<
} }
}; };
public arrayBuffer: AddArraySubTypeFieldDescriptor< public arrayBuffer: AddArrayBufferSubTypeFieldDescriptor<
TResult, TResult,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TAfterParsed,
Array.SubType.ArrayBuffer ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer
> = <TName extends string>( > = <TName extends string>(
name: TName, name: TName,
options: any options: any
) => { ) => {
return this.array(name, Array.SubType.ArrayBuffer, options); return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer, options);
}; };
public string: AddArraySubTypeFieldDescriptor< public string: AddArrayBufferSubTypeFieldDescriptor<
TResult, TResult,
TInit, TInit,
TExtra, TExtra,
TAfterParsed, TAfterParsed,
Array.SubType.String ArrayBufferLikeFieldDescriptor.SubType.String
> = <TName extends string>( > = <TName extends string>(
name: TName, name: TName,
options: any options: any
) => { ) => {
return this.array(name, Array.SubType.String, options); return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.String, options);
}; };
public extra<TValue extends Record< public extra<TValue extends Record<
@ -342,7 +373,7 @@ export default class Struct<
Exclude<keyof TValue, keyof TResult>>, Exclude<keyof TValue, keyof TResult>>,
never never
>>( >>(
value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>> value: TValue & ThisType<Overwrite<Overwrite<TExtra, TValue>, TResult>>
): Struct< ): Struct<
TResult, TResult,
TInit, TInit,
@ -371,73 +402,39 @@ export default class Struct<
return result; return result;
} }
private initializeField( private initializeObject(context: StructSerializationContext) {
context: StructSerializationContext, const object = createObjectWithRuntimeValues();
field: FieldDescriptorBase,
fieldTypeDefinition: FieldTypeDefinition<any, any>,
object: any,
value: any,
extra?: any
) {
if (fieldTypeDefinition.initialize) {
fieldTypeDefinition.initialize({
context,
extra,
field,
object,
options: this.options,
value,
});
} else {
setBackingField(object, field.name, value);
defineSimpleAccessors(object, field.name);
}
}
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> {
const object: any = {
[BackingField]: {},
};
Object.defineProperties(object, this._extra); Object.defineProperties(object, this._extra);
for (const field of this.fields) { for (const descriptor of this.fieldDescriptors) {
const fieldTypeDefinition = getFieldTypeDefinition(field.type); const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
this.initializeField(
context, const runtimeValue = new Constructor(descriptor, this.options, context, object);
field, setRuntimeValue(object, descriptor.name, runtimeValue);
fieldTypeDefinition,
object,
(init as any)[field.name]
);
} }
return object; return object;
} }
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> {
const object = this.initializeObject(context);
for (const { name: fieldName } of this.fieldDescriptors) {
const runtimeValue = getRuntimeValue(object, fieldName);
runtimeValue.set((init as any)[fieldName]);
}
return object as any;
}
public async deserialize( public async deserialize(
context: StructDeserializationContext context: StructDeserializationContext
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> { ): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
const object: any = { const object = this.initializeObject(context);
[BackingField]: {},
};
Object.defineProperties(object, this._extra);
for (const field of this.fields) { for (const { name: fieldName } of this.fieldDescriptors) {
const fieldTypeDefinition = getFieldTypeDefinition(field.type); const runtimeValue = getRuntimeValue(object, fieldName);
const { value, extra } = await fieldTypeDefinition.deserialize({ await runtimeValue.deserialize(context, object);
context,
field,
object,
options: this.options,
});
this.initializeField(
context,
field,
fieldTypeDefinition,
object,
value,
extra
);
} }
if (this._afterParsed) { if (this._afterParsed) {
@ -447,46 +444,30 @@ export default class Struct<
} }
} }
return object; return object as any;
} }
public serialize(init: TInit, context: StructSerializationContext): ArrayBuffer { public serialize(init: TInit, context: StructSerializationContext): ArrayBuffer {
const object = this.create(init, context) as any; const object = this.create(init, context) as any;
let size = this._size; let structSize = 0;
let fieldSize: number[] = []; const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = [];
for (let i = 0; i < this.fields.length; i += 1) {
const field = this.fields[i]; for (const { name: fieldName } of this.fieldDescriptors) {
const type = getFieldTypeDefinition(field.type); const runtimeValue = getRuntimeValue(object, fieldName);
if (type.getDynamicSize) { const size = runtimeValue.getSize();
fieldSize[i] = type.getDynamicSize({ fieldsInfo.push({ runtimeValue, size });
context, structSize += size;
field,
object,
options: this.options,
});
size += fieldSize[i];
} else {
fieldSize[i] = type.getSize({ field, options: this.options });
}
} }
const buffer = new ArrayBuffer(size); const buffer = new ArrayBuffer(structSize);
const dataView = new DataView(buffer); const dataView = new DataView(buffer);
let offset = 0; let offset = 0;
for (let i = 0; i < this.fields.length; i += 1) { for (const { runtimeValue, size } of fieldsInfo) {
const field = this.fields[i]; runtimeValue.serialize(dataView, offset, context);
const type = getFieldTypeDefinition(field.type); offset += size;
type.serialize({
context,
dataView,
field,
object,
offset,
options: this.options,
});
offset += fieldSize[i];
} }
return buffer; return buffer;
} }
} }

View file

@ -1,17 +0,0 @@
export interface StructSerializationContext {
encodeUtf8(input: string): ArrayBuffer;
}
export interface StructDeserializationContext extends StructSerializationContext {
decodeUtf8(buffer: ArrayBuffer): string;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
}
export interface StructOptions {
littleEndian: boolean;
}
export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false,
};

View file

@ -0,0 +1,138 @@
import { FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, StructDeserializationContext, StructSerializationContext } from '../runtime';
export namespace ArrayBufferLikeFieldDescriptor {
export enum SubType {
ArrayBuffer,
Uint8ClampedArray,
String,
}
export type TypeScriptType<TType extends SubType = SubType> =
TType extends SubType.ArrayBuffer ? ArrayBuffer :
TType extends SubType.Uint8ClampedArray ? Uint8ClampedArray :
TType extends SubType.String ? string :
never;
}
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;
}
const EmptyArrayBuffer = new ArrayBuffer(0);
const EmptyUint8ClampedArray = new Uint8ClampedArray(EmptyArrayBuffer);
const EmptyString = '';
export abstract class ArrayBufferLikeFieldRuntimeValue<TDescriptor extends ArrayBufferLikeFieldDescriptor>
extends FieldRuntimeValue<TDescriptor> {
protected arrayBuffer: ArrayBuffer | undefined;
protected typedArray: ArrayBufferView | undefined;
protected string: string | undefined;
protected getDeserializeSize(object: any): number {
return this.getSize();
}
public async deserialize(context: StructDeserializationContext, object: any): Promise<void> {
const size = this.getDeserializeSize(object);
this.arrayBuffer = undefined;
this.typedArray = undefined;
this.string = 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;
}
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');
}
}
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');
}
}
public set(value: unknown): void {
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');
}
}
new Uint8Array(dataView.buffer)
.set(new Uint8Array(this.arrayBuffer!), offset);
}
}

View file

@ -0,0 +1,37 @@
import { BuiltInFieldType, FieldDescriptorBaseOptions, GlobalStructFieldRuntimeTypeRegistry } from '../runtime';
import { ArrayBufferLikeFieldDescriptor, ArrayBufferLikeFieldRuntimeValue } from './array-buffer';
export namespace FixedLengthArrayBufferFieldDescriptor {
export interface Options extends FieldDescriptorBaseOptions {
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,
TType,
Record<TName, TTypeScriptType>,
Record<TName, TTypeScriptType>,
TOptions
> {
type: BuiltInFieldType.FixedLengthArrayBufferLike;
options: TOptions;
};
class FixedLengthArrayBufferFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<FixedLengthArrayBufferFieldDescriptor>{
public static getSize(descriptor: FixedLengthArrayBufferFieldDescriptor) {
return descriptor.options.length;
}
}
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.FixedLengthArrayBufferLike,
FixedLengthArrayBufferFieldRuntimeValue,
);

View file

@ -0,0 +1,5 @@
export * from './array-buffer';
export * from './fixed-length-array-buffer';
export * from './number';
export * from './utils';
export * from './variable-length-array-buffer';

View file

@ -0,0 +1,115 @@
import { StructDefaultOptions, StructDeserializationContext } from '../runtime';
import { NumberFieldSubType, NumberFieldRuntimeValue } from './number';
describe('Types', () => {
describe('Number', () => {
describe('NumberFieldSubType', () => {
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);
});
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);
});
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);
});
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);
});
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);
});
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);
});
});
describe('NumberFieldRuntimeValue', () => {
it('should deserialize Uint8', async () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]).buffer);
const context: StructDeserializationContext = {
read,
decodeUtf8(buffer) { throw new Error(''); },
encodeUtf8(input) { throw new Error(''); },
};
const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint8 } as any,
StructDefaultOptions,
undefined as any,
undefined as any,
);
await value.deserialize(context);
expect(value.get()).toBe(1);
expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint8.size);
});
it('should deserialize Uint16', async () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]).buffer);
const context: StructDeserializationContext = {
read,
decodeUtf8(buffer) { throw new Error(''); },
encodeUtf8(input) { throw new Error(''); },
};
const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint16 } as any,
StructDefaultOptions,
undefined as any,
undefined as any,
);
await value.deserialize(context);
expect(value.get()).toBe((1 << 8) | 2);
expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint16.size);
});
it('should deserialize Uint16LE', async () => {
const read = jest.fn((length: number) => new Uint8Array([1, 2, 3, 4]).buffer);
const context: StructDeserializationContext = {
read,
decodeUtf8(buffer) { throw new Error(''); },
encodeUtf8(input) { throw new Error(''); },
};
const value = new NumberFieldRuntimeValue(
{ subType: NumberFieldSubType.Uint16 } as any,
{ ...StructDefaultOptions, littleEndian: true },
undefined as any,
undefined as any,
);
await value.deserialize(context);
expect(value.get()).toBe((2 << 8) | 1);
expect(read).toBeCalledTimes(1);
expect(read).lastCalledWith(NumberFieldSubType.Uint16.size);
});
});
});
});

View file

@ -0,0 +1,96 @@
import { BuiltInFieldType, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, StructDeserializationContext } from '../runtime';
export type DataViewGetters =
{ [TKey in keyof DataView]: TKey extends `get${string}` ? TKey : never }[keyof DataView];
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');
public static readonly Uint16 = new NumberFieldSubType<number>(2, 'getUint16', 'setUint16');
public static readonly Int32 = new NumberFieldSubType<number>(4, 'getInt32', 'setInt32');
public static readonly Uint32 = new NumberFieldSubType<number>(4, 'getUint32', 'setUint32');
public static readonly Int64 = new NumberFieldSubType<bigint>(8, 'getBigInt64', 'setBigInt64');
public static readonly Uint64 = new NumberFieldSubType<bigint>(8, 'getBigUint64', 'setBigUint64');
public readonly value!: TTypeScriptType;
public readonly size: number;
public readonly dataViewGetter: DataViewGetters;
public readonly dataViewSetter: DataViewSetters;
public constructor(
size: number,
dataViewGetter: DataViewGetters,
dataViewSetter: DataViewSetters
) {
this.size = size;
this.dataViewGetter = dataViewGetter;
this.dataViewSetter = dataViewSetter;
}
}
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
> {
type: BuiltInFieldType.Number;
subType: TSubType;
}
export class NumberFieldRuntimeValue extends FieldRuntimeValue<NumberFieldDescriptor> {
public static getSize(descriptor: NumberFieldDescriptor): number {
return descriptor.subType.size;
}
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](
0,
this.options.littleEndian
);
}
public get(): unknown {
return this.value;
}
public set(value: unknown): void {
this.value = value as number | bigint;
}
public serialize(dataView: DataView, offset: number): void {
// `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)(
offset,
this.value!,
this.options.littleEndian
);
}
}
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.Number,
NumberFieldRuntimeValue,
);

View file

@ -0,0 +1,47 @@
/**
* When evaluating a very complex generic type alias,
* tell TypeScript to use `T`, instead of current type alias' name, as the result type name
*
* Example:
*
* ```ts
* type WithIdentity<T> = Identity<SomeType<T>>;
* type WithoutIdentity<T> = SomeType<T>;
*
* type WithIdentityResult = WithIdentity<number>;
* // Hover on this one shows `SomeType<number>`
*
* type WithoutIdentityResult = WithoutIdentity<number>;
* // Hover on this one shows `WithoutIdentity<number>`
* ```
*/
export type Identity<T> = T;
/**
* Collapse an intersection type (`{ foo: string } & { bar: number }`) to a simple type (`{ foo: string, bar: number }`)
*/
export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
/**
* Overwrite fields in `TBase` with fields in `TNew`
*/
export type Overwrite<TBase extends object, TNew extends object> =
Evaluate<Omit<TBase, keyof TNew> & TNew>;
/**
* Remove fields with `never` type
*/
export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
/**
* Generates a type. Useful in generic type inference.
*/
export function placeholder<T>(): T {
return undefined as unknown as T;
}
/**
* Extract keys of fields in `T` that has type `TValue`
*/
export type KeysOfType<T, TValue> =
{ [TKey in keyof T]: T[TKey] extends TValue ? TKey : never }[keyof T];

View file

@ -0,0 +1,126 @@
import { BuiltInFieldType, FieldDescriptorBaseOptions, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructOptions, StructSerializationContext } from '../runtime';
import { ArrayBufferLikeFieldDescriptor, ArrayBufferLikeFieldRuntimeValue } from './array-buffer';
import { NumberFieldDescriptor, NumberFieldRuntimeValue } from './number';
import { KeysOfType } from './utils';
export namespace VariableLengthArrayBufferFieldDescriptor {
export interface Options<
TInit = object,
TLengthField extends KeysOfType<TInit, number | string> = any,
> extends FieldDescriptorBaseOptions {
lengthField: TLengthField;
}
}
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;
public constructor(
descriptor: NumberFieldDescriptor,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any,
arrayBufferValue: VariableLengthArrayBufferFieldRuntimeValue,
) {
super(descriptor, options, context, object);
this.arrayBufferValue = arrayBufferValue;
}
getDeserializeSize() {
return this.value;
}
get() {
return this.arrayBufferValue.getSize();
}
set() { }
serialize(dataView: DataView, offset: number) {
this.value = this.get();
super.serialize(dataView, offset);
}
}
class VariableLengthArrayBufferFieldRuntimeValue
extends ArrayBufferLikeFieldRuntimeValue<VariableLengthArrayBufferFieldDescriptor> {
public static getSize() {
return 0;
}
protected length: number | undefined;
protected lengthFieldValue: VariableLengthArrayBufferLengthFieldRuntimeValue;
public constructor(
descriptor: VariableLengthArrayBufferFieldDescriptor,
options: Readonly<StructOptions>,
context: StructSerializationContext,
object: any
) {
super(descriptor, options, context, object);
const lengthField = this.descriptor.options.lengthField;
const oldValue = getRuntimeValue(object, lengthField) as NumberFieldRuntimeValue;
this.lengthFieldValue = new VariableLengthArrayBufferLengthFieldRuntimeValue(
oldValue.descriptor,
this.options,
this.context,
object,
this
);
setRuntimeValue(object, lengthField, this.lengthFieldValue);
}
protected getDeserializeSize() {
const value = this.lengthFieldValue.getDeserializeSize() as number;
return value;
}
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!);
this.length = this.arrayBuffer.byteLength;
break;
}
}
return this.length;
}
public set(value: unknown) {
super.set(value);
this.length = undefined;
}
}
GlobalStructFieldRuntimeTypeRegistry.register(
BuiltInFieldType.VariableLengthArrayBufferLike,
VariableLengthArrayBufferFieldRuntimeValue,
);

View file

@ -1,12 +0,0 @@
export type Identity<T> = T;
export type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
export type Overwrite<TBase extends object, TNew extends object> =
Evaluate<Omit<TBase, keyof TNew> & TNew>;
export type OmitNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
export function placeholder<T>(): T {
return undefined as unknown as T;
}

View file

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.esm.json",
"compilerOptions": {
"composite": false,
"module": "CommonJS",
"outDir": "cjs",
"declaration": false,
"declarationDir": null,
"declarationMap": false
}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "esm",
"declarationDir": "dts",
"types": [],
},
"include": [
"src"
],
"exclude": [
"src/**/*.spec.ts"
]
}

View file

@ -1,7 +1,10 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "./tsconfig.esm.json",
"compilerOptions": { "compilerOptions": {
"outDir": "lib", // /* Redirect output structure to the directory. */ "noEmit": true,
"rootDir": "./src" // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "types": [
} "jest"
]
},
"exclude": []
} }

View file

@ -1,62 +1,48 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": true,
/* Basic Options */ /* Basic Options */
"incremental": true, // /* Enable incremental compilation */ "target": "ES5", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"target": "ES2018", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */ "lib": [ // /* Specify library files to be included in the compilation. */
"ESNext" "ESNext"
], ],
// "allowJs": true, /* Allow javascript files to be compiled. */ "rootDir": "src",
// "checkJs": true, /* Report errors in .js files. */ "outDir": "esm",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"declarationDir": "dts",
"declarationMap": true,
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, // /* Generates corresponding '.d.ts' file. */
"declarationMap": true, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, // /* Generates corresponding '.map' file. */
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
// "outDir": "lib", /* Redirect output structure to the directory. */
// "rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"composite": true, // /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */ // "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, // /* Import emit helpers from 'tslib'. */ "importHelpers": true, // /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, // /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ "downlevelIteration": true, // /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"isolatedModules": true, // /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ "isolatedModules": true, // /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
"skipLibCheck": true, // /* Skip type checking of all declaration files (*.d.ts). */
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, // /* Enable all strict type-checking options. */ "strict": true, // /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */ "noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ "types": [], // /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */ },
"forceConsistentCasingInFileNames": true ///* Disallow inconsistently-cased references to the same file. */ "include": [
} "src"
],
"exclude": [
"src/**/*.spec.ts"
]
} }