mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
refactor(struct): rework on type plugin system
This commit is contained in:
parent
389157ac87
commit
8eed9aa3b7
51 changed files with 10921 additions and 951 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
|||
lib
|
||||
cjs
|
||||
esm
|
||||
dts
|
||||
node_modules
|
||||
*.log
|
||||
tsconfig.tsbuildinfo
|
||||
*.tsbuildinfo
|
||||
/package-lock.json
|
||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -45,6 +45,7 @@
|
|||
"sysui",
|
||||
"tcpip",
|
||||
"tinyh",
|
||||
"tsbuildinfo",
|
||||
"uifabric",
|
||||
"webadb",
|
||||
"websockify",
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"main": "cjs/index.js",
|
||||
"module": "esm/index.js",
|
||||
"types": "dts/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"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. */
|
||||
"outDir": "esm",
|
||||
"declarationDir": "dts",
|
||||
"rootDir": "./src",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"types": [
|
||||
"@types/w3c-web-usb",
|
||||
"@yume-chan/adb"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../adb/tsconfig.json"
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"main": "cjs/index.js",
|
||||
"module": "esm/index.js",
|
||||
"types": "dts/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
||||
|
|
|
@ -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 { BufferedStream } from './stream';
|
||||
|
||||
|
@ -11,7 +11,7 @@ export enum AdbCommand {
|
|||
Write = 0x45545257, // 'WRTE'
|
||||
}
|
||||
|
||||
const AdbPacketWithoutPayload =
|
||||
const AdbPacketHeader =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('command', undefined)
|
||||
.uint32('arg0')
|
||||
|
@ -21,13 +21,8 @@ const AdbPacketWithoutPayload =
|
|||
.int32('magic');
|
||||
|
||||
const AdbPacketStruct =
|
||||
AdbPacketWithoutPayload
|
||||
.arrayBuffer('payload', { lengthField: 'payloadLength' })
|
||||
.afterParsed((value) => {
|
||||
if (value[BackingField].magic !== value.magic) {
|
||||
throw new Error('Invalid command');
|
||||
}
|
||||
});
|
||||
AdbPacketHeader
|
||||
.arrayBuffer('payload', { lengthField: 'payloadLength' });
|
||||
|
||||
export type AdbPacket = StructValueType<typeof AdbPacketStruct>;
|
||||
|
||||
|
@ -37,7 +32,7 @@ export namespace AdbPacket {
|
|||
export function create(
|
||||
init: AdbPacketInit,
|
||||
calculateChecksum: boolean,
|
||||
backend: AdbBackend
|
||||
backend: AdbBackend,
|
||||
): AdbPacket {
|
||||
let checksum: number;
|
||||
if (calculateChecksum && init.payload) {
|
||||
|
@ -85,8 +80,9 @@ export namespace AdbPacket {
|
|||
|
||||
export async function write(packet: AdbPacket, backend: AdbBackend): Promise<void> {
|
||||
// Write payload separately to avoid an extra copy
|
||||
await backend.write(AdbPacketWithoutPayload.serialize(packet, backend));
|
||||
if (packet.payload) {
|
||||
const header = AdbPacketHeader.serialize(packet, backend);
|
||||
await backend.write(header);
|
||||
if (packet.payload.byteLength) {
|
||||
await backend.write(packet.payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ export interface AdbIncomingSocketEventArgs {
|
|||
socket: AdbSocket;
|
||||
}
|
||||
|
||||
const EmptyArrayBuffer = new ArrayBuffer(0);
|
||||
|
||||
export class AdbPacketDispatcher extends AutoDisposable {
|
||||
// ADB socket id starts from 1
|
||||
// (0 means open failed)
|
||||
|
@ -222,10 +224,10 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
packetOrCommand: AdbPacketInit | AdbCommand,
|
||||
arg0?: number,
|
||||
arg1?: number,
|
||||
payload?: string | ArrayBuffer
|
||||
payload: string | ArrayBuffer = EmptyArrayBuffer,
|
||||
): Promise<void> {
|
||||
let init: AdbPacketInit;
|
||||
if (arguments.length === 1) {
|
||||
if (arg0 === undefined) {
|
||||
init = packetOrCommand as AdbPacketInit;
|
||||
} else {
|
||||
init = {
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"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. */
|
||||
"target": "ES2016",
|
||||
"outDir": "esm",
|
||||
"declarationDir": "dts",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../event/tsconfig.json"
|
||||
"path": "../event/tsconfig.esm.json"
|
||||
},
|
||||
{
|
||||
"path": "../struct/tsconfig.json"
|
||||
"path": "../struct/tsconfig.esm.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 React from 'react';
|
||||
import { ExternalLink } from '../components';
|
||||
|
|
|
@ -2,15 +2,32 @@
|
|||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"target": "ES2016",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"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 */
|
||||
"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": [
|
||||
{
|
||||
"path": "../adb-backend-web/tsconfig.json"
|
||||
|
|
4798
packages/event/package-lock.json
generated
4798
packages/event/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,25 +5,39 @@
|
|||
"keywords": [
|
||||
"event"
|
||||
],
|
||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||
"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": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"build:watch": "tsc -b -w"
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||
"directory": "packages/event"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "4.1.3"
|
||||
"main": "cjs/index.js",
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
11
packages/event/tsconfig.cjs.json
Normal file
11
packages/event/tsconfig.cjs.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.esm.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"module": "CommonJS",
|
||||
"outDir": "cjs",
|
||||
"declaration": false,
|
||||
"declarationDir": null,
|
||||
"declarationMap": false
|
||||
}
|
||||
}
|
15
packages/event/tsconfig.esm.json
Normal file
15
packages/event/tsconfig.esm.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "esm",
|
||||
"declarationDir": "dts",
|
||||
"types": [],
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"extends": "./tsconfig.esm.json",
|
||||
"compilerOptions": {
|
||||
"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. */
|
||||
}
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"exclude": []
|
||||
}
|
||||
|
|
10
packages/struct/.npmignore
Normal file
10
packages/struct/.npmignore
Normal file
|
@ -0,0 +1,10 @@
|
|||
.github
|
||||
.vscode
|
||||
coverage
|
||||
src/**.spec.ts
|
||||
|
||||
jest.config.js
|
||||
pnpm-lock.yaml
|
||||
renovate.json
|
||||
tsconfig.json
|
||||
*.tsbuildinfo
|
4
packages/struct/jest.config.js
Normal file
4
packages/struct/jest.config.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
4798
packages/struct/package-lock.json
generated
4798
packages/struct/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -6,25 +6,39 @@
|
|||
"structure",
|
||||
"typescript"
|
||||
],
|
||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||
"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": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"build:watch": "tsc -b -w"
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||
"directory": "packages/struct"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "4.1.3"
|
||||
"main": "cjs/index.js",
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; };
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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';
|
|
@ -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
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
|
@ -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
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
|
@ -1,6 +1,4 @@
|
|||
export * from './backing-field';
|
||||
export * from './field';
|
||||
export * from './runtime';
|
||||
export * from './struct';
|
||||
export { default as Struct } from './struct';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
|
14
packages/struct/src/runtime/context.spec.ts
Normal file
14
packages/struct/src/runtime/context.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
45
packages/struct/src/runtime/context.ts
Normal file
45
packages/struct/src/runtime/context.ts
Normal 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,
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
export enum FieldType {
|
||||
export enum BuiltInFieldType {
|
||||
Number,
|
||||
FixedLengthArray,
|
||||
VariableLengthArray,
|
||||
FixedLengthArrayBufferLike,
|
||||
VariableLengthArrayBufferLike,
|
||||
}
|
||||
|
||||
export interface FieldDescriptorBaseOptions {
|
||||
|
@ -14,7 +14,7 @@ export interface FieldDescriptorBase<
|
|||
TInitObject = {},
|
||||
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
|
||||
> {
|
||||
type: FieldType | string;
|
||||
type: BuiltInFieldType | string;
|
||||
|
||||
name: TName;
|
||||
|
5
packages/struct/src/runtime/index.ts
Normal file
5
packages/struct/src/runtime/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export * from './context';
|
||||
export * from './descriptor';
|
||||
export * from './registry';
|
||||
export * from './runtime-type';
|
||||
export * from './runtime-value';
|
61
packages/struct/src/runtime/registry.spec.ts
Normal file
61
packages/struct/src/runtime/registry.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
26
packages/struct/src/runtime/registry.ts
Normal file
26
packages/struct/src/runtime/registry.ts
Normal 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();
|
27
packages/struct/src/runtime/runtime-type.spec.ts
Normal file
27
packages/struct/src/runtime/runtime-type.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
45
packages/struct/src/runtime/runtime-type.ts
Normal file
45
packages/struct/src/runtime/runtime-type.ts
Normal 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;
|
||||
}
|
34
packages/struct/src/runtime/runtime-value.spec.ts
Normal file
34
packages/struct/src/runtime/runtime-value.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
28
packages/struct/src/runtime/runtime-value.ts
Normal file
28
packages/struct/src/runtime/runtime-value.ts
Normal 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); },
|
||||
});
|
||||
}
|
|
@ -1,135 +1,166 @@
|
|||
import { BackingField, defineSimpleAccessors, setBackingField, WithBackingField } from './backing-field';
|
||||
import { Array, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FieldTypeDefinition, FixedLengthArray, getFieldTypeDefinition, Number, VariableLengthArray } from './field';
|
||||
import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './types';
|
||||
import { Evaluate, Identity, OmitNever, Overwrite } from './utils';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Extract the value type of the specified `Struct`
|
||||
*
|
||||
* The lack of generic constraint is on purpose to allow `StructLike` types
|
||||
*/
|
||||
export type StructValueType<T> =
|
||||
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>> =
|
||||
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
|
||||
|
||||
interface AddArrayFieldDescriptor<
|
||||
TResult extends object,
|
||||
TInit extends object,
|
||||
TExtra extends object,
|
||||
TAfterParsed
|
||||
> {
|
||||
<
|
||||
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,
|
||||
/**
|
||||
* Create a new `Struct` type with `TDescriptor` appended
|
||||
*/
|
||||
type AddFieldDescriptor<
|
||||
TValue extends object,
|
||||
TInit extends object,
|
||||
TExtra extends object,
|
||||
TAfterParsed,
|
||||
TType extends Array.SubType
|
||||
> {
|
||||
<
|
||||
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
|
||||
> =
|
||||
TDescriptor extends FieldDescriptorBase> =
|
||||
Identity<Struct<
|
||||
Evaluate<TResult & Exclude<TDescriptor['resultObject'], undefined>>,
|
||||
OmitNever<TInit & Exclude<TDescriptor['initObject'], undefined>>,
|
||||
// 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>
|
||||
>,
|
||||
TExtra,
|
||||
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> =
|
||||
(this: WithBackingField<TResult>, object: WithBackingField<TResult>) => TAfterParsed;
|
||||
(this: TResult, object: TResult) => TAfterParsed;
|
||||
|
||||
export default class Struct<
|
||||
TResult extends object = {},
|
||||
|
@ -142,19 +173,19 @@ export default class Struct<
|
|||
private _size = 0;
|
||||
public get size() { return this._size; }
|
||||
|
||||
private fields: FieldDescriptorBase[] = [];
|
||||
private fieldDescriptors: FieldDescriptorBase[] = [];
|
||||
|
||||
private _extra: PropertyDescriptorMap = {};
|
||||
|
||||
private _afterParsed?: StructAfterParsed<any, any>;
|
||||
|
||||
public constructor(options: Partial<StructOptions> = StructDefaultOptions) {
|
||||
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.fields = this.fields.slice();
|
||||
result.fieldDescriptors = this.fieldDescriptors.slice();
|
||||
result._size = this._size;
|
||||
result._extra = this._extra;
|
||||
result._afterParsed = this._afterParsed;
|
||||
|
@ -162,13 +193,13 @@ export default class Struct<
|
|||
}
|
||||
|
||||
public field<TDescriptor extends FieldDescriptorBase>(
|
||||
field: TDescriptor,
|
||||
): MergeStruct<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
|
||||
descriptor: TDescriptor,
|
||||
): AddFieldDescriptor<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
|
||||
const result = this.clone();
|
||||
result.fields.push(field);
|
||||
result.fieldDescriptors.push(descriptor);
|
||||
|
||||
const definition = getFieldTypeDefinition(field.type);
|
||||
const size = definition.getSize({ field, options: this.options });
|
||||
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
|
||||
const size = Constructor.getSize(descriptor, this.options);
|
||||
result._size += size;
|
||||
|
||||
return result;
|
||||
|
@ -176,16 +207,16 @@ export default class Struct<
|
|||
|
||||
private number<
|
||||
TName extends string,
|
||||
TSubType extends Number.SubType = Number.SubType,
|
||||
TTypeScriptType = Number.TypeScriptType<TSubType>
|
||||
TType extends NumberFieldSubType = NumberFieldSubType,
|
||||
TTypeScriptType = TType['value']
|
||||
>(
|
||||
name: TName,
|
||||
type: TSubType,
|
||||
type: TType,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.field<Number<TName, TSubType, TTypeScriptType>>({
|
||||
type: FieldType.Number,
|
||||
return this.field<NumberFieldDescriptor<TName, TType, TTypeScriptType>>({
|
||||
type: BuiltInFieldType.Number,
|
||||
name,
|
||||
subType: type,
|
||||
options,
|
||||
|
@ -194,7 +225,7 @@ export default class Struct<
|
|||
|
||||
public uint8<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint8>
|
||||
TTypeScriptType = (typeof NumberFieldSubType)['Uint8']['value']
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
|
@ -202,7 +233,7 @@ export default class Struct<
|
|||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Uint8,
|
||||
NumberFieldSubType.Uint8,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
|
@ -210,7 +241,7 @@ export default class Struct<
|
|||
|
||||
public uint16<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint16>
|
||||
TTypeScriptType = (typeof NumberFieldSubType)['Uint16']['value']
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
|
@ -218,7 +249,7 @@ export default class Struct<
|
|||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Uint16,
|
||||
NumberFieldSubType.Uint16,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
|
@ -226,7 +257,7 @@ export default class Struct<
|
|||
|
||||
public int32<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Int32>
|
||||
TTypeScriptType = (typeof NumberFieldSubType)['Int32']['value']
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
|
@ -234,7 +265,7 @@ export default class Struct<
|
|||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Int32,
|
||||
NumberFieldSubType.Int32,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
|
@ -242,7 +273,23 @@ export default class Struct<
|
|||
|
||||
public uint32<
|
||||
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,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
|
@ -250,7 +297,7 @@ export default class Struct<
|
|||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Uint32,
|
||||
NumberFieldSubType.Int64,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
|
@ -258,7 +305,7 @@ export default class Struct<
|
|||
|
||||
public uint64<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint64>
|
||||
TTypeScriptType = (typeof NumberFieldSubType)['Uint64']['value']
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
|
@ -266,43 +313,27 @@ export default class Struct<
|
|||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Uint64,
|
||||
NumberFieldSubType.Uint64,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
}
|
||||
|
||||
public int64<
|
||||
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> = (
|
||||
private arrayBufferLike: AddArrayBufferFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
|
||||
name: string,
|
||||
type: Array.SubType,
|
||||
options: FixedLengthArray.Options | VariableLengthArray.Options
|
||||
type: ArrayBufferLikeFieldDescriptor.SubType,
|
||||
options: FixedLengthArrayBufferFieldDescriptor.Options | VariableLengthArrayBufferFieldDescriptor.Options
|
||||
): Struct<any, any, any, any> => {
|
||||
if ('length' in options) {
|
||||
return this.field<FixedLengthArray>({
|
||||
type: FieldType.FixedLengthArray,
|
||||
return this.field<FixedLengthArrayBufferFieldDescriptor>({
|
||||
type: BuiltInFieldType.FixedLengthArrayBufferLike,
|
||||
name,
|
||||
subType: type,
|
||||
options: options,
|
||||
});
|
||||
} else {
|
||||
return this.field<VariableLengthArray>({
|
||||
type: FieldType.VariableLengthArray,
|
||||
return this.field<VariableLengthArrayBufferFieldDescriptor>({
|
||||
type: BuiltInFieldType.VariableLengthArrayBufferLike,
|
||||
name,
|
||||
subType: type,
|
||||
options: options,
|
||||
|
@ -310,30 +341,30 @@ export default class Struct<
|
|||
}
|
||||
};
|
||||
|
||||
public arrayBuffer: AddArraySubTypeFieldDescriptor<
|
||||
public arrayBuffer: AddArrayBufferSubTypeFieldDescriptor<
|
||||
TResult,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
Array.SubType.ArrayBuffer
|
||||
ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer
|
||||
> = <TName extends string>(
|
||||
name: TName,
|
||||
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,
|
||||
TInit,
|
||||
TExtra,
|
||||
TAfterParsed,
|
||||
Array.SubType.String
|
||||
ArrayBufferLikeFieldDescriptor.SubType.String
|
||||
> = <TName extends string>(
|
||||
name: TName,
|
||||
options: any
|
||||
) => {
|
||||
return this.array(name, Array.SubType.String, options);
|
||||
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.String, options);
|
||||
};
|
||||
|
||||
public extra<TValue extends Record<
|
||||
|
@ -342,7 +373,7 @@ export default class Struct<
|
|||
Exclude<keyof TValue, keyof TResult>>,
|
||||
never
|
||||
>>(
|
||||
value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>>
|
||||
value: TValue & ThisType<Overwrite<Overwrite<TExtra, TValue>, TResult>>
|
||||
): Struct<
|
||||
TResult,
|
||||
TInit,
|
||||
|
@ -371,73 +402,39 @@ export default class Struct<
|
|||
return result;
|
||||
}
|
||||
|
||||
private initializeField(
|
||||
context: StructSerializationContext,
|
||||
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]: {},
|
||||
};
|
||||
private initializeObject(context: StructSerializationContext) {
|
||||
const object = createObjectWithRuntimeValues();
|
||||
Object.defineProperties(object, this._extra);
|
||||
|
||||
for (const field of this.fields) {
|
||||
const fieldTypeDefinition = getFieldTypeDefinition(field.type);
|
||||
this.initializeField(
|
||||
context,
|
||||
field,
|
||||
fieldTypeDefinition,
|
||||
object,
|
||||
(init as any)[field.name]
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
context: StructDeserializationContext
|
||||
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
|
||||
const object: any = {
|
||||
[BackingField]: {},
|
||||
};
|
||||
Object.defineProperties(object, this._extra);
|
||||
const object = this.initializeObject(context);
|
||||
|
||||
for (const field of this.fields) {
|
||||
const fieldTypeDefinition = getFieldTypeDefinition(field.type);
|
||||
const { value, extra } = await fieldTypeDefinition.deserialize({
|
||||
context,
|
||||
field,
|
||||
object,
|
||||
options: this.options,
|
||||
});
|
||||
this.initializeField(
|
||||
context,
|
||||
field,
|
||||
fieldTypeDefinition,
|
||||
object,
|
||||
value,
|
||||
extra
|
||||
);
|
||||
for (const { name: fieldName } of this.fieldDescriptors) {
|
||||
const runtimeValue = getRuntimeValue(object, fieldName);
|
||||
await runtimeValue.deserialize(context, object);
|
||||
}
|
||||
|
||||
if (this._afterParsed) {
|
||||
|
@ -447,46 +444,30 @@ export default class Struct<
|
|||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
return object as any;
|
||||
}
|
||||
|
||||
public serialize(init: TInit, context: StructSerializationContext): ArrayBuffer {
|
||||
const object = this.create(init, context) as any;
|
||||
|
||||
let size = this._size;
|
||||
let fieldSize: number[] = [];
|
||||
for (let i = 0; i < this.fields.length; i += 1) {
|
||||
const field = this.fields[i];
|
||||
const type = getFieldTypeDefinition(field.type);
|
||||
if (type.getDynamicSize) {
|
||||
fieldSize[i] = type.getDynamicSize({
|
||||
context,
|
||||
field,
|
||||
object,
|
||||
options: this.options,
|
||||
});
|
||||
size += fieldSize[i];
|
||||
} else {
|
||||
fieldSize[i] = type.getSize({ field, options: this.options });
|
||||
}
|
||||
let structSize = 0;
|
||||
const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = [];
|
||||
|
||||
for (const { name: fieldName } of this.fieldDescriptors) {
|
||||
const runtimeValue = getRuntimeValue(object, fieldName);
|
||||
const size = runtimeValue.getSize();
|
||||
fieldsInfo.push({ runtimeValue, size });
|
||||
structSize += size;
|
||||
}
|
||||
|
||||
const buffer = new ArrayBuffer(size);
|
||||
const buffer = new ArrayBuffer(structSize);
|
||||
const dataView = new DataView(buffer);
|
||||
let offset = 0;
|
||||
for (let i = 0; i < this.fields.length; i += 1) {
|
||||
const field = this.fields[i];
|
||||
const type = getFieldTypeDefinition(field.type);
|
||||
type.serialize({
|
||||
context,
|
||||
dataView,
|
||||
field,
|
||||
object,
|
||||
offset,
|
||||
options: this.options,
|
||||
});
|
||||
offset += fieldSize[i];
|
||||
for (const { runtimeValue, size } of fieldsInfo) {
|
||||
runtimeValue.serialize(dataView, offset, context);
|
||||
offset += size;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
138
packages/struct/src/types/array-buffer.ts
Normal file
138
packages/struct/src/types/array-buffer.ts
Normal 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);
|
||||
}
|
||||
}
|
37
packages/struct/src/types/fixed-length-array-buffer.ts
Normal file
37
packages/struct/src/types/fixed-length-array-buffer.ts
Normal 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,
|
||||
);
|
5
packages/struct/src/types/index.ts
Normal file
5
packages/struct/src/types/index.ts
Normal 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';
|
115
packages/struct/src/types/number.spec.ts
Normal file
115
packages/struct/src/types/number.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
96
packages/struct/src/types/number.ts
Normal file
96
packages/struct/src/types/number.ts
Normal 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,
|
||||
);
|
47
packages/struct/src/types/utils.ts
Normal file
47
packages/struct/src/types/utils.ts
Normal 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];
|
126
packages/struct/src/types/variable-length-array-buffer.ts
Normal file
126
packages/struct/src/types/variable-length-array-buffer.ts
Normal 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,
|
||||
);
|
|
@ -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;
|
||||
}
|
11
packages/struct/tsconfig.cjs.json
Normal file
11
packages/struct/tsconfig.cjs.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.esm.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"module": "CommonJS",
|
||||
"outDir": "cjs",
|
||||
"declaration": false,
|
||||
"declarationDir": null,
|
||||
"declarationMap": false
|
||||
}
|
||||
}
|
15
packages/struct/tsconfig.esm.json
Normal file
15
packages/struct/tsconfig.esm.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "esm",
|
||||
"declarationDir": "dts",
|
||||
"types": [],
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"extends": "./tsconfig.esm.json",
|
||||
"compilerOptions": {
|
||||
"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. */
|
||||
}
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"exclude": []
|
||||
}
|
||||
|
|
|
@ -1,62 +1,48 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
/* Basic Options */
|
||||
"incremental": true, // /* Enable incremental compilation */
|
||||
"target": "ES2018", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"target": "ES5", // /* 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'. */
|
||||
"lib": [ // /* Specify library files to be included in the compilation. */
|
||||
"ESNext"
|
||||
],
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
"rootDir": "src",
|
||||
"outDir": "esm",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "dts",
|
||||
"declarationMap": true,
|
||||
// "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. */
|
||||
"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'). */
|
||||
"skipLibCheck": true, // /* Skip type checking of all declaration files (*.d.ts). */
|
||||
/* 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 */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "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'. */
|
||||
// "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. */
|
||||
// "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. */
|
||||
"types": [], // /* Type declaration files to be included in compilation. */
|
||||
"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. */
|
||||
// "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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue