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
|
node_modules
|
||||||
*.log
|
*.log
|
||||||
tsconfig.tsbuildinfo
|
*.tsbuildinfo
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -45,6 +45,7 @@
|
||||||
"sysui",
|
"sysui",
|
||||||
"tcpip",
|
"tcpip",
|
||||||
"tinyh",
|
"tinyh",
|
||||||
|
"tsbuildinfo",
|
||||||
"uifabric",
|
"uifabric",
|
||||||
"webadb",
|
"webadb",
|
||||||
"websockify",
|
"websockify",
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/index.js",
|
"main": "cjs/index.js",
|
||||||
|
"module": "esm/index.js",
|
||||||
|
"types": "dts/index.d.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "lib", // /* Redirect output structure to the directory. */
|
"outDir": "esm",
|
||||||
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"declarationDir": "dts",
|
||||||
|
"rootDir": "./src",
|
||||||
"lib": [
|
"lib": [
|
||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"@types/w3c-web-usb",
|
||||||
|
"@yume-chan/adb"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../adb/tsconfig.json"
|
"path": "../adb/tsconfig.json"
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/index.js",
|
"main": "cjs/index.js",
|
||||||
|
"module": "esm/index.js",
|
||||||
|
"types": "dts/index.d.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BackingField, Struct, StructInitType, StructValueType } from '@yume-chan/struct';
|
import { Struct, StructInitType, StructValueType } from '@yume-chan/struct';
|
||||||
import { AdbBackend } from './backend';
|
import { AdbBackend } from './backend';
|
||||||
import { BufferedStream } from './stream';
|
import { BufferedStream } from './stream';
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export enum AdbCommand {
|
||||||
Write = 0x45545257, // 'WRTE'
|
Write = 0x45545257, // 'WRTE'
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdbPacketWithoutPayload =
|
const AdbPacketHeader =
|
||||||
new Struct({ littleEndian: true })
|
new Struct({ littleEndian: true })
|
||||||
.uint32('command', undefined)
|
.uint32('command', undefined)
|
||||||
.uint32('arg0')
|
.uint32('arg0')
|
||||||
|
@ -21,13 +21,8 @@ const AdbPacketWithoutPayload =
|
||||||
.int32('magic');
|
.int32('magic');
|
||||||
|
|
||||||
const AdbPacketStruct =
|
const AdbPacketStruct =
|
||||||
AdbPacketWithoutPayload
|
AdbPacketHeader
|
||||||
.arrayBuffer('payload', { lengthField: 'payloadLength' })
|
.arrayBuffer('payload', { lengthField: 'payloadLength' });
|
||||||
.afterParsed((value) => {
|
|
||||||
if (value[BackingField].magic !== value.magic) {
|
|
||||||
throw new Error('Invalid command');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AdbPacket = StructValueType<typeof AdbPacketStruct>;
|
export type AdbPacket = StructValueType<typeof AdbPacketStruct>;
|
||||||
|
|
||||||
|
@ -37,7 +32,7 @@ export namespace AdbPacket {
|
||||||
export function create(
|
export function create(
|
||||||
init: AdbPacketInit,
|
init: AdbPacketInit,
|
||||||
calculateChecksum: boolean,
|
calculateChecksum: boolean,
|
||||||
backend: AdbBackend
|
backend: AdbBackend,
|
||||||
): AdbPacket {
|
): AdbPacket {
|
||||||
let checksum: number;
|
let checksum: number;
|
||||||
if (calculateChecksum && init.payload) {
|
if (calculateChecksum && init.payload) {
|
||||||
|
@ -85,8 +80,9 @@ export namespace AdbPacket {
|
||||||
|
|
||||||
export async function write(packet: AdbPacket, backend: AdbBackend): Promise<void> {
|
export async function write(packet: AdbPacket, backend: AdbBackend): Promise<void> {
|
||||||
// Write payload separately to avoid an extra copy
|
// Write payload separately to avoid an extra copy
|
||||||
await backend.write(AdbPacketWithoutPayload.serialize(packet, backend));
|
const header = AdbPacketHeader.serialize(packet, backend);
|
||||||
if (packet.payload) {
|
await backend.write(header);
|
||||||
|
if (packet.payload.byteLength) {
|
||||||
await backend.write(packet.payload);
|
await backend.write(packet.payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ export interface AdbIncomingSocketEventArgs {
|
||||||
socket: AdbSocket;
|
socket: AdbSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EmptyArrayBuffer = new ArrayBuffer(0);
|
||||||
|
|
||||||
export class AdbPacketDispatcher extends AutoDisposable {
|
export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
// ADB socket id starts from 1
|
// ADB socket id starts from 1
|
||||||
// (0 means open failed)
|
// (0 means open failed)
|
||||||
|
@ -222,10 +224,10 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
packetOrCommand: AdbPacketInit | AdbCommand,
|
packetOrCommand: AdbPacketInit | AdbCommand,
|
||||||
arg0?: number,
|
arg0?: number,
|
||||||
arg1?: number,
|
arg1?: number,
|
||||||
payload?: string | ArrayBuffer
|
payload: string | ArrayBuffer = EmptyArrayBuffer,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let init: AdbPacketInit;
|
let init: AdbPacketInit;
|
||||||
if (arguments.length === 1) {
|
if (arg0 === undefined) {
|
||||||
init = packetOrCommand as AdbPacketInit;
|
init = packetOrCommand as AdbPacketInit;
|
||||||
} else {
|
} else {
|
||||||
init = {
|
init = {
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "lib", // /* Redirect output structure to the directory. */
|
"target": "ES2016",
|
||||||
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"outDir": "esm",
|
||||||
|
"declarationDir": "dts",
|
||||||
|
"rootDir": "./src"
|
||||||
},
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../event/tsconfig.json"
|
"path": "../event/tsconfig.esm.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../struct/tsconfig.json"
|
"path": "../struct/tsconfig.esm.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Icon, MessageBar, StackItem, TooltipHost, Text, Separator } from '@fluentui/react';
|
import { Icon, MessageBar, Separator, TooltipHost } from '@fluentui/react';
|
||||||
import { AdbFeatures } from '@yume-chan/adb';
|
import { AdbFeatures } from '@yume-chan/adb';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ExternalLink } from '../components';
|
import { ExternalLink } from '../components';
|
||||||
|
|
|
@ -2,15 +2,32 @@
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
"target": "ES2016",
|
||||||
"lib": [
|
"lib": [
|
||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM"
|
||||||
],
|
],
|
||||||
"jsx": "react", // /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
"jsx": "react", // /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
"declaration": false, // /* Generates corresponding '.d.ts' file. */
|
|
||||||
"declarationMap": false, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
|
||||||
"composite": false, // /* Enable project compilation */
|
"composite": false, // /* Enable project compilation */
|
||||||
|
"types": [
|
||||||
|
"@types/node",
|
||||||
|
"@types/react",
|
||||||
|
"@types/react-dom",
|
||||||
|
"@types/react-router-dom",
|
||||||
|
"@fluentui/react",
|
||||||
|
"@yume-chan/adb",
|
||||||
|
"@yume-chan/adb-backend-web",
|
||||||
|
"@yume-chan/async-operation-manager",
|
||||||
|
"@yume-chan/event",
|
||||||
|
"@yume-chan/struct"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../adb-backend-web/tsconfig.json"
|
"path": "../adb-backend-web/tsconfig.json"
|
||||||
|
|
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": [
|
"keywords": [
|
||||||
"event"
|
"event"
|
||||||
],
|
],
|
||||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
|
||||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/index.js",
|
"author": {
|
||||||
|
"name": "Simon Chan",
|
||||||
|
"email": "cnsimonchan@live.com",
|
||||||
|
"url": "https://chensi.moe/blog"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/yume-chan/ya-webadb/tree/master/packages/event#readme",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||||
},
|
"directory": "packages/event"
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -b",
|
|
||||||
"build:watch": "tsc -b -w"
|
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"main": "cjs/index.js",
|
||||||
"typescript": "4.1.3"
|
"module": "esm/index.js",
|
||||||
|
"types": "dts/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf {cjs,esm,dts,*.tsbuildinfo} && tsc -b tsconfig.esm.json tsconfig.cjs.json",
|
||||||
|
"build:watch": "tsc -b -w tsconfig.esm.json tsconfig.cjs.json",
|
||||||
|
"test": "jest",
|
||||||
|
"coverage": "jest --coverage",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.0.3"
|
"tslib": "2.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "26.0.19",
|
||||||
|
"jest": "26.6.3",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"ts-jest": "26.4.4",
|
||||||
|
"typescript": "4.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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": {
|
"compilerOptions": {
|
||||||
"outDir": "lib", // /* Redirect output structure to the directory. */
|
"noEmit": true,
|
||||||
"rootDir": "./src" // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"types": [
|
||||||
}
|
"jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": []
|
||||||
}
|
}
|
||||||
|
|
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",
|
"structure",
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
|
||||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/index.js",
|
"author": {
|
||||||
|
"name": "Simon Chan",
|
||||||
|
"email": "cnsimonchan@live.com",
|
||||||
|
"url": "https://chensi.moe/blog"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/yume-chan/ya-webadb/tree/master/packages/struct#readme",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/yume-chan/ya-webadb.git"
|
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||||
},
|
"directory": "packages/struct"
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -b",
|
|
||||||
"build:watch": "tsc -b -w"
|
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"main": "cjs/index.js",
|
||||||
"typescript": "4.1.3"
|
"module": "esm/index.js",
|
||||||
|
"types": "dts/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf {cjs,esm,dts,*.tsbuildinfo} && tsc -b tsconfig.esm.json tsconfig.cjs.json",
|
||||||
|
"build:watch": "tsc -b -w tsconfig.esm.json tsconfig.cjs.json",
|
||||||
|
"test": "jest",
|
||||||
|
"coverage": "jest --coverage",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.0.3"
|
"tslib": "2.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "26.0.19",
|
||||||
|
"jest": "26.6.3",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"ts-jest": "26.4.4",
|
||||||
|
"typescript": "4.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 './runtime';
|
||||||
export * from './field';
|
|
||||||
export * from './struct';
|
export * from './struct';
|
||||||
export { default as Struct } from './struct';
|
export { default as Struct } from './struct';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
|
||||||
|
|
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,
|
Number,
|
||||||
FixedLengthArray,
|
FixedLengthArrayBufferLike,
|
||||||
VariableLengthArray,
|
VariableLengthArrayBufferLike,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldDescriptorBaseOptions {
|
export interface FieldDescriptorBaseOptions {
|
||||||
|
@ -14,7 +14,7 @@ export interface FieldDescriptorBase<
|
||||||
TInitObject = {},
|
TInitObject = {},
|
||||||
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
|
TOptions extends FieldDescriptorBaseOptions = FieldDescriptorBaseOptions
|
||||||
> {
|
> {
|
||||||
type: FieldType | string;
|
type: BuiltInFieldType | string;
|
||||||
|
|
||||||
name: TName;
|
name: TName;
|
||||||
|
|
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 { BuiltInFieldType, createObjectWithRuntimeValues, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldRuntimeValue, getRuntimeValue, GlobalStructFieldRuntimeTypeRegistry, setRuntimeValue, StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './runtime';
|
||||||
import { Array, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, FieldTypeDefinition, FixedLengthArray, getFieldTypeDefinition, Number, VariableLengthArray } from './field';
|
import { ArrayBufferLikeFieldDescriptor, Evaluate, FixedLengthArrayBufferFieldDescriptor, Identity, KeysOfType, NumberFieldDescriptor, NumberFieldSubType, OmitNever, Overwrite, VariableLengthArrayBufferFieldDescriptor } from './types';
|
||||||
import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './types';
|
|
||||||
import { Evaluate, Identity, OmitNever, Overwrite } from './utils';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the value type of the specified `Struct`
|
||||||
|
*
|
||||||
|
* The lack of generic constraint is on purpose to allow `StructLike` types
|
||||||
|
*/
|
||||||
export type StructValueType<T> =
|
export type StructValueType<T> =
|
||||||
T extends { deserialize(context: StructDeserializationContext): Promise<infer R>; } ? R : never;
|
T extends { deserialize(context: StructDeserializationContext): Promise<infer R>; } ? R : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the init type of the specified `Struct`
|
||||||
|
*/
|
||||||
export type StructInitType<T extends Struct<object, object, object, unknown>> =
|
export type StructInitType<T extends Struct<object, object, object, unknown>> =
|
||||||
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
|
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
|
||||||
|
|
||||||
interface AddArrayFieldDescriptor<
|
/**
|
||||||
TResult extends object,
|
* Create a new `Struct` type with `TDescriptor` appended
|
||||||
TInit extends object,
|
*/
|
||||||
TExtra extends object,
|
type AddFieldDescriptor<
|
||||||
TAfterParsed
|
TValue extends object,
|
||||||
> {
|
|
||||||
<
|
|
||||||
TName extends string,
|
|
||||||
TType extends Array.SubType,
|
|
||||||
TTypeScriptType = Array.TypeScriptType<TType>
|
|
||||||
>(
|
|
||||||
name: TName,
|
|
||||||
type: TType,
|
|
||||||
options: FixedLengthArray.Options,
|
|
||||||
typescriptType?: () => TTypeScriptType,
|
|
||||||
): MergeStruct<
|
|
||||||
TResult,
|
|
||||||
TInit,
|
|
||||||
TExtra,
|
|
||||||
TAfterParsed,
|
|
||||||
FixedLengthArray<
|
|
||||||
TName,
|
|
||||||
TType,
|
|
||||||
TTypeScriptType
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
|
|
||||||
<
|
|
||||||
TName extends string,
|
|
||||||
TType extends Array.SubType,
|
|
||||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
|
|
||||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
|
||||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
|
||||||
>(
|
|
||||||
name: TName,
|
|
||||||
type: TType,
|
|
||||||
options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
|
|
||||||
typescriptType?: () => TTypeScriptType,
|
|
||||||
): MergeStruct<
|
|
||||||
TResult,
|
|
||||||
TInit,
|
|
||||||
TExtra,
|
|
||||||
TAfterParsed,
|
|
||||||
VariableLengthArray<
|
|
||||||
TName,
|
|
||||||
TType,
|
|
||||||
TInit,
|
|
||||||
TLengthField,
|
|
||||||
TEmptyBehavior,
|
|
||||||
TTypeScriptType
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddArraySubTypeFieldDescriptor<
|
|
||||||
TResult extends object,
|
|
||||||
TInit extends object,
|
TInit extends object,
|
||||||
TExtra extends object,
|
TExtra extends object,
|
||||||
TAfterParsed,
|
TAfterParsed,
|
||||||
TType extends Array.SubType
|
TDescriptor extends FieldDescriptorBase> =
|
||||||
> {
|
|
||||||
<
|
|
||||||
TName extends string,
|
|
||||||
TTypeScriptType = Array.TypeScriptType<TType>
|
|
||||||
>(
|
|
||||||
name: TName,
|
|
||||||
options: FixedLengthArray.Options,
|
|
||||||
typescriptType?: () => TTypeScriptType,
|
|
||||||
): MergeStruct<
|
|
||||||
TResult,
|
|
||||||
TInit,
|
|
||||||
TExtra,
|
|
||||||
TAfterParsed,
|
|
||||||
FixedLengthArray<
|
|
||||||
TName,
|
|
||||||
TType,
|
|
||||||
TTypeScriptType
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
|
|
||||||
<
|
|
||||||
TName extends string,
|
|
||||||
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
|
|
||||||
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
|
|
||||||
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
|
|
||||||
>(
|
|
||||||
name: TName,
|
|
||||||
options: VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
|
|
||||||
_typescriptType?: TTypeScriptType,
|
|
||||||
): MergeStruct<
|
|
||||||
TResult,
|
|
||||||
TInit,
|
|
||||||
TExtra,
|
|
||||||
TAfterParsed,
|
|
||||||
VariableLengthArray<
|
|
||||||
TName,
|
|
||||||
TType,
|
|
||||||
TInit,
|
|
||||||
TLengthField,
|
|
||||||
TEmptyBehavior,
|
|
||||||
TTypeScriptType
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MergeStruct<
|
|
||||||
TResult extends object,
|
|
||||||
TInit extends object,
|
|
||||||
TExtra extends object,
|
|
||||||
TAfterParsed,
|
|
||||||
TDescriptor extends FieldDescriptorBase
|
|
||||||
> =
|
|
||||||
Identity<Struct<
|
Identity<Struct<
|
||||||
Evaluate<TResult & Exclude<TDescriptor['resultObject'], undefined>>,
|
// Merge two types
|
||||||
OmitNever<TInit & Exclude<TDescriptor['initObject'], undefined>>,
|
Evaluate<
|
||||||
|
TValue &
|
||||||
|
// `TDescriptor.resultObject` is optional, so remove `undefined` from its type
|
||||||
|
Exclude<TDescriptor['resultObject'], undefined>
|
||||||
|
>,
|
||||||
|
// `TDescriptor.initObject` signals removal of fields by setting its type to `never`
|
||||||
|
// I don't `Evaluate` here, because if I do, the result type will become too complex,
|
||||||
|
// and TypeScript will refuse to evaluate it.
|
||||||
|
OmitNever<
|
||||||
|
TInit &
|
||||||
|
// `TDescriptor.initObject` is optional, so remove `undefined` from its type
|
||||||
|
Exclude<TDescriptor['initObject'], undefined>
|
||||||
|
>,
|
||||||
TExtra,
|
TExtra,
|
||||||
TAfterParsed
|
TAfterParsed
|
||||||
>>;
|
>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overload methods to add an array typed field
|
||||||
|
*/
|
||||||
|
interface AddArrayBufferFieldDescriptor<
|
||||||
|
TValue extends object,
|
||||||
|
TInit extends object,
|
||||||
|
TExtra extends object,
|
||||||
|
TAfterParsed
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Append a fixed-length array to the `Struct`
|
||||||
|
*
|
||||||
|
* @param name Name of the field
|
||||||
|
* @param type `Array.SubType.ArrayBuffer` or `Array.SubType.String`
|
||||||
|
* @param options Fixed-length array options
|
||||||
|
* @param typescriptType Type of the field in TypeScript.
|
||||||
|
* For example, if this field is a string, you can declare it as a string enum or literal union.
|
||||||
|
*/
|
||||||
|
<
|
||||||
|
TName extends string,
|
||||||
|
TType extends ArrayBufferLikeFieldDescriptor.SubType,
|
||||||
|
TTypeScriptType extends ArrayBufferLikeFieldDescriptor.TypeScriptType<TType> = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
type: TType,
|
||||||
|
options: FixedLengthArrayBufferFieldDescriptor.Options,
|
||||||
|
typescriptType?: TTypeScriptType,
|
||||||
|
): AddFieldDescriptor<
|
||||||
|
TValue,
|
||||||
|
TInit,
|
||||||
|
TExtra,
|
||||||
|
TAfterParsed,
|
||||||
|
FixedLengthArrayBufferFieldDescriptor<
|
||||||
|
TName,
|
||||||
|
TType,
|
||||||
|
TTypeScriptType
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a variable-length array to the `Struct`
|
||||||
|
*/
|
||||||
|
<
|
||||||
|
TName extends string,
|
||||||
|
TType extends ArrayBufferLikeFieldDescriptor.SubType,
|
||||||
|
TLengthField extends KeysOfType<TInit, number | string>,
|
||||||
|
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
type: TType,
|
||||||
|
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>,
|
||||||
|
typescriptType?: TTypeScriptType,
|
||||||
|
): AddFieldDescriptor<
|
||||||
|
TValue,
|
||||||
|
TInit,
|
||||||
|
TExtra,
|
||||||
|
TAfterParsed,
|
||||||
|
VariableLengthArrayBufferFieldDescriptor<
|
||||||
|
TName,
|
||||||
|
TType,
|
||||||
|
TInit,
|
||||||
|
TLengthField,
|
||||||
|
TTypeScriptType
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddArrayBufferSubTypeFieldDescriptor<
|
||||||
|
TResult extends object,
|
||||||
|
TInit extends object,
|
||||||
|
TExtra extends object,
|
||||||
|
TAfterParsed,
|
||||||
|
TType extends ArrayBufferLikeFieldDescriptor.SubType
|
||||||
|
> {
|
||||||
|
<
|
||||||
|
TName extends string,
|
||||||
|
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
options: FixedLengthArrayBufferFieldDescriptor.Options,
|
||||||
|
typescriptType?: TTypeScriptType,
|
||||||
|
): AddFieldDescriptor<
|
||||||
|
TResult,
|
||||||
|
TInit,
|
||||||
|
TExtra,
|
||||||
|
TAfterParsed,
|
||||||
|
FixedLengthArrayBufferFieldDescriptor<
|
||||||
|
TName,
|
||||||
|
TType,
|
||||||
|
TTypeScriptType
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
<
|
||||||
|
TName extends string,
|
||||||
|
TLengthField extends KeysOfType<TInit, number | string>,
|
||||||
|
TTypeScriptType = ArrayBufferLikeFieldDescriptor.TypeScriptType<TType>
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
options: VariableLengthArrayBufferFieldDescriptor.Options<TInit, TLengthField>,
|
||||||
|
_typescriptType?: TTypeScriptType,
|
||||||
|
): AddFieldDescriptor<
|
||||||
|
TResult,
|
||||||
|
TInit,
|
||||||
|
TExtra,
|
||||||
|
TAfterParsed,
|
||||||
|
VariableLengthArrayBufferFieldDescriptor<
|
||||||
|
TName,
|
||||||
|
TType,
|
||||||
|
TInit,
|
||||||
|
TLengthField,
|
||||||
|
TTypeScriptType
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
export type StructAfterParsed<TResult, TAfterParsed> =
|
export type StructAfterParsed<TResult, TAfterParsed> =
|
||||||
(this: WithBackingField<TResult>, object: WithBackingField<TResult>) => TAfterParsed;
|
(this: TResult, object: TResult) => TAfterParsed;
|
||||||
|
|
||||||
export default class Struct<
|
export default class Struct<
|
||||||
TResult extends object = {},
|
TResult extends object = {},
|
||||||
|
@ -142,19 +173,19 @@ export default class Struct<
|
||||||
private _size = 0;
|
private _size = 0;
|
||||||
public get size() { return this._size; }
|
public get size() { return this._size; }
|
||||||
|
|
||||||
private fields: FieldDescriptorBase[] = [];
|
private fieldDescriptors: FieldDescriptorBase[] = [];
|
||||||
|
|
||||||
private _extra: PropertyDescriptorMap = {};
|
private _extra: PropertyDescriptorMap = {};
|
||||||
|
|
||||||
private _afterParsed?: StructAfterParsed<any, any>;
|
private _afterParsed?: StructAfterParsed<any, any>;
|
||||||
|
|
||||||
public constructor(options: Partial<StructOptions> = StructDefaultOptions) {
|
public constructor(options?: Partial<StructOptions>) {
|
||||||
this.options = { ...StructDefaultOptions, ...options };
|
this.options = { ...StructDefaultOptions, ...options };
|
||||||
}
|
}
|
||||||
|
|
||||||
private clone(): Struct<any, any, any, any> {
|
private clone(): Struct<any, any, any, any> {
|
||||||
const result = new Struct<any, any, any, any>(this.options);
|
const result = new Struct<any, any, any, any>(this.options);
|
||||||
result.fields = this.fields.slice();
|
result.fieldDescriptors = this.fieldDescriptors.slice();
|
||||||
result._size = this._size;
|
result._size = this._size;
|
||||||
result._extra = this._extra;
|
result._extra = this._extra;
|
||||||
result._afterParsed = this._afterParsed;
|
result._afterParsed = this._afterParsed;
|
||||||
|
@ -162,13 +193,13 @@ export default class Struct<
|
||||||
}
|
}
|
||||||
|
|
||||||
public field<TDescriptor extends FieldDescriptorBase>(
|
public field<TDescriptor extends FieldDescriptorBase>(
|
||||||
field: TDescriptor,
|
descriptor: TDescriptor,
|
||||||
): MergeStruct<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
|
): AddFieldDescriptor<TResult, TInit, TExtra, TAfterParsed, TDescriptor> {
|
||||||
const result = this.clone();
|
const result = this.clone();
|
||||||
result.fields.push(field);
|
result.fieldDescriptors.push(descriptor);
|
||||||
|
|
||||||
const definition = getFieldTypeDefinition(field.type);
|
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
|
||||||
const size = definition.getSize({ field, options: this.options });
|
const size = Constructor.getSize(descriptor, this.options);
|
||||||
result._size += size;
|
result._size += size;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -176,16 +207,16 @@ export default class Struct<
|
||||||
|
|
||||||
private number<
|
private number<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TSubType extends Number.SubType = Number.SubType,
|
TType extends NumberFieldSubType = NumberFieldSubType,
|
||||||
TTypeScriptType = Number.TypeScriptType<TSubType>
|
TTypeScriptType = TType['value']
|
||||||
>(
|
>(
|
||||||
name: TName,
|
name: TName,
|
||||||
type: TSubType,
|
type: TType,
|
||||||
options: FieldDescriptorBaseOptions = {},
|
options: FieldDescriptorBaseOptions = {},
|
||||||
_typescriptType?: TTypeScriptType,
|
_typescriptType?: TTypeScriptType,
|
||||||
) {
|
) {
|
||||||
return this.field<Number<TName, TSubType, TTypeScriptType>>({
|
return this.field<NumberFieldDescriptor<TName, TType, TTypeScriptType>>({
|
||||||
type: FieldType.Number,
|
type: BuiltInFieldType.Number,
|
||||||
name,
|
name,
|
||||||
subType: type,
|
subType: type,
|
||||||
options,
|
options,
|
||||||
|
@ -194,7 +225,7 @@ export default class Struct<
|
||||||
|
|
||||||
public uint8<
|
public uint8<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint8>
|
TTypeScriptType = (typeof NumberFieldSubType)['Uint8']['value']
|
||||||
>(
|
>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: FieldDescriptorBaseOptions = {},
|
options: FieldDescriptorBaseOptions = {},
|
||||||
|
@ -202,7 +233,7 @@ export default class Struct<
|
||||||
) {
|
) {
|
||||||
return this.number(
|
return this.number(
|
||||||
name,
|
name,
|
||||||
Number.SubType.Uint8,
|
NumberFieldSubType.Uint8,
|
||||||
options,
|
options,
|
||||||
_typescriptType
|
_typescriptType
|
||||||
);
|
);
|
||||||
|
@ -210,7 +241,7 @@ export default class Struct<
|
||||||
|
|
||||||
public uint16<
|
public uint16<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint16>
|
TTypeScriptType = (typeof NumberFieldSubType)['Uint16']['value']
|
||||||
>(
|
>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: FieldDescriptorBaseOptions = {},
|
options: FieldDescriptorBaseOptions = {},
|
||||||
|
@ -218,7 +249,7 @@ export default class Struct<
|
||||||
) {
|
) {
|
||||||
return this.number(
|
return this.number(
|
||||||
name,
|
name,
|
||||||
Number.SubType.Uint16,
|
NumberFieldSubType.Uint16,
|
||||||
options,
|
options,
|
||||||
_typescriptType
|
_typescriptType
|
||||||
);
|
);
|
||||||
|
@ -226,7 +257,7 @@ export default class Struct<
|
||||||
|
|
||||||
public int32<
|
public int32<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Int32>
|
TTypeScriptType = (typeof NumberFieldSubType)['Int32']['value']
|
||||||
>(
|
>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: FieldDescriptorBaseOptions = {},
|
options: FieldDescriptorBaseOptions = {},
|
||||||
|
@ -234,7 +265,7 @@ export default class Struct<
|
||||||
) {
|
) {
|
||||||
return this.number(
|
return this.number(
|
||||||
name,
|
name,
|
||||||
Number.SubType.Int32,
|
NumberFieldSubType.Int32,
|
||||||
options,
|
options,
|
||||||
_typescriptType
|
_typescriptType
|
||||||
);
|
);
|
||||||
|
@ -242,7 +273,23 @@ export default class Struct<
|
||||||
|
|
||||||
public uint32<
|
public uint32<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint32>
|
TTypeScriptType = (typeof NumberFieldSubType)['Uint32']['value']
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
options: FieldDescriptorBaseOptions = {},
|
||||||
|
typescriptType?: TTypeScriptType,
|
||||||
|
) {
|
||||||
|
return this.number(
|
||||||
|
name,
|
||||||
|
NumberFieldSubType.Uint32,
|
||||||
|
options,
|
||||||
|
typescriptType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int64<
|
||||||
|
TName extends string,
|
||||||
|
TTypeScriptType = (typeof NumberFieldSubType)['Int64']['value']
|
||||||
>(
|
>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: FieldDescriptorBaseOptions = {},
|
options: FieldDescriptorBaseOptions = {},
|
||||||
|
@ -250,7 +297,7 @@ export default class Struct<
|
||||||
) {
|
) {
|
||||||
return this.number(
|
return this.number(
|
||||||
name,
|
name,
|
||||||
Number.SubType.Uint32,
|
NumberFieldSubType.Int64,
|
||||||
options,
|
options,
|
||||||
_typescriptType
|
_typescriptType
|
||||||
);
|
);
|
||||||
|
@ -258,7 +305,7 @@ export default class Struct<
|
||||||
|
|
||||||
public uint64<
|
public uint64<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint64>
|
TTypeScriptType = (typeof NumberFieldSubType)['Uint64']['value']
|
||||||
>(
|
>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: FieldDescriptorBaseOptions = {},
|
options: FieldDescriptorBaseOptions = {},
|
||||||
|
@ -266,43 +313,27 @@ export default class Struct<
|
||||||
) {
|
) {
|
||||||
return this.number(
|
return this.number(
|
||||||
name,
|
name,
|
||||||
Number.SubType.Uint64,
|
NumberFieldSubType.Uint64,
|
||||||
options,
|
options,
|
||||||
_typescriptType
|
_typescriptType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int64<
|
private arrayBufferLike: AddArrayBufferFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
|
||||||
TName extends string,
|
|
||||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Int64>
|
|
||||||
>(
|
|
||||||
name: TName,
|
|
||||||
options: FieldDescriptorBaseOptions = {},
|
|
||||||
_typescriptType?: TTypeScriptType,
|
|
||||||
) {
|
|
||||||
return this.number(
|
|
||||||
name,
|
|
||||||
Number.SubType.Int64,
|
|
||||||
options,
|
|
||||||
_typescriptType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private array: AddArrayFieldDescriptor<TResult, TInit, TExtra, TAfterParsed> = (
|
|
||||||
name: string,
|
name: string,
|
||||||
type: Array.SubType,
|
type: ArrayBufferLikeFieldDescriptor.SubType,
|
||||||
options: FixedLengthArray.Options | VariableLengthArray.Options
|
options: FixedLengthArrayBufferFieldDescriptor.Options | VariableLengthArrayBufferFieldDescriptor.Options
|
||||||
): Struct<any, any, any, any> => {
|
): Struct<any, any, any, any> => {
|
||||||
if ('length' in options) {
|
if ('length' in options) {
|
||||||
return this.field<FixedLengthArray>({
|
return this.field<FixedLengthArrayBufferFieldDescriptor>({
|
||||||
type: FieldType.FixedLengthArray,
|
type: BuiltInFieldType.FixedLengthArrayBufferLike,
|
||||||
name,
|
name,
|
||||||
subType: type,
|
subType: type,
|
||||||
options: options,
|
options: options,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return this.field<VariableLengthArray>({
|
return this.field<VariableLengthArrayBufferFieldDescriptor>({
|
||||||
type: FieldType.VariableLengthArray,
|
type: BuiltInFieldType.VariableLengthArrayBufferLike,
|
||||||
name,
|
name,
|
||||||
subType: type,
|
subType: type,
|
||||||
options: options,
|
options: options,
|
||||||
|
@ -310,30 +341,30 @@ export default class Struct<
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public arrayBuffer: AddArraySubTypeFieldDescriptor<
|
public arrayBuffer: AddArrayBufferSubTypeFieldDescriptor<
|
||||||
TResult,
|
TResult,
|
||||||
TInit,
|
TInit,
|
||||||
TExtra,
|
TExtra,
|
||||||
TAfterParsed,
|
TAfterParsed,
|
||||||
Array.SubType.ArrayBuffer
|
ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer
|
||||||
> = <TName extends string>(
|
> = <TName extends string>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: any
|
options: any
|
||||||
) => {
|
) => {
|
||||||
return this.array(name, Array.SubType.ArrayBuffer, options);
|
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.ArrayBuffer, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
public string: AddArraySubTypeFieldDescriptor<
|
public string: AddArrayBufferSubTypeFieldDescriptor<
|
||||||
TResult,
|
TResult,
|
||||||
TInit,
|
TInit,
|
||||||
TExtra,
|
TExtra,
|
||||||
TAfterParsed,
|
TAfterParsed,
|
||||||
Array.SubType.String
|
ArrayBufferLikeFieldDescriptor.SubType.String
|
||||||
> = <TName extends string>(
|
> = <TName extends string>(
|
||||||
name: TName,
|
name: TName,
|
||||||
options: any
|
options: any
|
||||||
) => {
|
) => {
|
||||||
return this.array(name, Array.SubType.String, options);
|
return this.arrayBufferLike(name, ArrayBufferLikeFieldDescriptor.SubType.String, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
public extra<TValue extends Record<
|
public extra<TValue extends Record<
|
||||||
|
@ -342,7 +373,7 @@ export default class Struct<
|
||||||
Exclude<keyof TValue, keyof TResult>>,
|
Exclude<keyof TValue, keyof TResult>>,
|
||||||
never
|
never
|
||||||
>>(
|
>>(
|
||||||
value: TValue & ThisType<WithBackingField<Overwrite<Overwrite<TExtra, TValue>, TResult>>>
|
value: TValue & ThisType<Overwrite<Overwrite<TExtra, TValue>, TResult>>
|
||||||
): Struct<
|
): Struct<
|
||||||
TResult,
|
TResult,
|
||||||
TInit,
|
TInit,
|
||||||
|
@ -371,73 +402,39 @@ export default class Struct<
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeField(
|
private initializeObject(context: StructSerializationContext) {
|
||||||
context: StructSerializationContext,
|
const object = createObjectWithRuntimeValues();
|
||||||
field: FieldDescriptorBase,
|
|
||||||
fieldTypeDefinition: FieldTypeDefinition<any, any>,
|
|
||||||
object: any,
|
|
||||||
value: any,
|
|
||||||
extra?: any
|
|
||||||
) {
|
|
||||||
if (fieldTypeDefinition.initialize) {
|
|
||||||
fieldTypeDefinition.initialize({
|
|
||||||
context,
|
|
||||||
extra,
|
|
||||||
field,
|
|
||||||
object,
|
|
||||||
options: this.options,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setBackingField(object, field.name, value);
|
|
||||||
defineSimpleAccessors(object, field.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> {
|
|
||||||
const object: any = {
|
|
||||||
[BackingField]: {},
|
|
||||||
};
|
|
||||||
Object.defineProperties(object, this._extra);
|
Object.defineProperties(object, this._extra);
|
||||||
|
|
||||||
for (const field of this.fields) {
|
for (const descriptor of this.fieldDescriptors) {
|
||||||
const fieldTypeDefinition = getFieldTypeDefinition(field.type);
|
const Constructor = GlobalStructFieldRuntimeTypeRegistry.get(descriptor.type);
|
||||||
this.initializeField(
|
|
||||||
context,
|
const runtimeValue = new Constructor(descriptor, this.options, context, object);
|
||||||
field,
|
setRuntimeValue(object, descriptor.name, runtimeValue);
|
||||||
fieldTypeDefinition,
|
|
||||||
object,
|
|
||||||
(init as any)[field.name]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public create(init: TInit, context: StructSerializationContext): Overwrite<TExtra, TResult> {
|
||||||
|
const object = this.initializeObject(context);
|
||||||
|
|
||||||
|
for (const { name: fieldName } of this.fieldDescriptors) {
|
||||||
|
const runtimeValue = getRuntimeValue(object, fieldName);
|
||||||
|
runtimeValue.set((init as any)[fieldName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return object as any;
|
||||||
|
}
|
||||||
|
|
||||||
public async deserialize(
|
public async deserialize(
|
||||||
context: StructDeserializationContext
|
context: StructDeserializationContext
|
||||||
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
|
): Promise<TAfterParsed extends undefined ? Overwrite<TExtra, TResult> : TAfterParsed> {
|
||||||
const object: any = {
|
const object = this.initializeObject(context);
|
||||||
[BackingField]: {},
|
|
||||||
};
|
|
||||||
Object.defineProperties(object, this._extra);
|
|
||||||
|
|
||||||
for (const field of this.fields) {
|
for (const { name: fieldName } of this.fieldDescriptors) {
|
||||||
const fieldTypeDefinition = getFieldTypeDefinition(field.type);
|
const runtimeValue = getRuntimeValue(object, fieldName);
|
||||||
const { value, extra } = await fieldTypeDefinition.deserialize({
|
await runtimeValue.deserialize(context, object);
|
||||||
context,
|
|
||||||
field,
|
|
||||||
object,
|
|
||||||
options: this.options,
|
|
||||||
});
|
|
||||||
this.initializeField(
|
|
||||||
context,
|
|
||||||
field,
|
|
||||||
fieldTypeDefinition,
|
|
||||||
object,
|
|
||||||
value,
|
|
||||||
extra
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._afterParsed) {
|
if (this._afterParsed) {
|
||||||
|
@ -447,46 +444,30 @@ export default class Struct<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
return object as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
public serialize(init: TInit, context: StructSerializationContext): ArrayBuffer {
|
public serialize(init: TInit, context: StructSerializationContext): ArrayBuffer {
|
||||||
const object = this.create(init, context) as any;
|
const object = this.create(init, context) as any;
|
||||||
|
|
||||||
let size = this._size;
|
let structSize = 0;
|
||||||
let fieldSize: number[] = [];
|
const fieldsInfo: { runtimeValue: FieldRuntimeValue, size: number; }[] = [];
|
||||||
for (let i = 0; i < this.fields.length; i += 1) {
|
|
||||||
const field = this.fields[i];
|
for (const { name: fieldName } of this.fieldDescriptors) {
|
||||||
const type = getFieldTypeDefinition(field.type);
|
const runtimeValue = getRuntimeValue(object, fieldName);
|
||||||
if (type.getDynamicSize) {
|
const size = runtimeValue.getSize();
|
||||||
fieldSize[i] = type.getDynamicSize({
|
fieldsInfo.push({ runtimeValue, size });
|
||||||
context,
|
structSize += size;
|
||||||
field,
|
|
||||||
object,
|
|
||||||
options: this.options,
|
|
||||||
});
|
|
||||||
size += fieldSize[i];
|
|
||||||
} else {
|
|
||||||
fieldSize[i] = type.getSize({ field, options: this.options });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = new ArrayBuffer(size);
|
const buffer = new ArrayBuffer(structSize);
|
||||||
const dataView = new DataView(buffer);
|
const dataView = new DataView(buffer);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for (let i = 0; i < this.fields.length; i += 1) {
|
for (const { runtimeValue, size } of fieldsInfo) {
|
||||||
const field = this.fields[i];
|
runtimeValue.serialize(dataView, offset, context);
|
||||||
const type = getFieldTypeDefinition(field.type);
|
offset += size;
|
||||||
type.serialize({
|
|
||||||
context,
|
|
||||||
dataView,
|
|
||||||
field,
|
|
||||||
object,
|
|
||||||
offset,
|
|
||||||
options: this.options,
|
|
||||||
});
|
|
||||||
offset += fieldSize[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
"compilerOptions": {
|
||||||
"outDir": "lib", // /* Redirect output structure to the directory. */
|
"noEmit": true,
|
||||||
"rootDir": "./src" // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"types": [
|
||||||
}
|
"jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,48 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"incremental": true, // /* Enable incremental compilation */
|
"target": "ES5", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||||
"target": "ES2018", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||||
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
"lib": [ // /* Specify library files to be included in the compilation. */
|
||||||
"lib": [ // /* Specify library files to be included in the compilation. */
|
|
||||||
"ESNext"
|
"ESNext"
|
||||||
],
|
],
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"rootDir": "src",
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
"outDir": "esm",
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
"sourceMap": true,
|
||||||
"declaration": true, // /* Generates corresponding '.d.ts' file. */
|
"removeComments": true,
|
||||||
"declarationMap": true, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
"declaration": true,
|
||||||
"sourceMap": true, // /* Generates corresponding '.map' file. */
|
"declarationDir": "dts",
|
||||||
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
|
"declarationMap": true,
|
||||||
// "outDir": "lib", /* Redirect output structure to the directory. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
// "rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
"composite": true, // /* Enable project compilation */
|
"importHelpers": true, // /* Import emit helpers from 'tslib'. */
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
"downlevelIteration": true, // /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
"isolatedModules": true, // /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
"skipLibCheck": true, // /* Skip type checking of all declaration files (*.d.ts). */
|
||||||
"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'. */
|
|
||||||
"isolatedModules": true, // /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true, // /* Enable all strict type-checking options. */
|
"strict": true, // /* Enable all strict type-checking options. */
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
|
"noImplicitReturns": true, // /* Report error when not all code paths in function return a value. */
|
||||||
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
|
"noFallthroughCasesInSwitch": true, // /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
"types": [], // /* Type declaration files to be included in compilation. */
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
},
|
||||||
/* Source Map Options */
|
"include": [
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
"src"
|
||||||
// "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. */
|
"exclude": [
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
"src/**/*.spec.ts"
|
||||||
/* 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. */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue