refactor(struct): remove context interfaces

This commit is contained in:
Simon Chan 2022-01-08 18:51:14 +08:00
parent 949aaab818
commit 08767c7b71
20 changed files with 429 additions and 333 deletions

View file

@ -23,12 +23,14 @@ specifiers:
'@rush-temp/ts-package-builder': file:./projects/ts-package-builder.tgz
'@rush-temp/unofficial-adb-book': file:./projects/unofficial-adb-book.tgz
'@svgr/webpack': ^5.5.0
'@types/bluebird': ^3.5.36
'@types/dom-webcodecs': ^0.1.3
'@types/jest': ^26.0.23
'@types/node': ^16.9.1
'@types/jest': ^27.4.0
'@types/node': ^16.11.19
'@types/react': 17.0.27
'@types/w3c-web-usb': ^1.0.4
'@yume-chan/async': ^2.1.4
bluebird: ^3.7.2
clsx: ^1.1.1
eslint: 7.32.0
eslint-config-next: ^12.0.7
@ -78,12 +80,14 @@ dependencies:
'@rush-temp/ts-package-builder': file:projects/ts-package-builder.tgz
'@rush-temp/unofficial-adb-book': file:projects/unofficial-adb-book.tgz_fe5950f86df08edf541b29db75ee5ce3
'@svgr/webpack': 5.5.0
'@types/bluebird': 3.5.36
'@types/dom-webcodecs': 0.1.3
'@types/jest': 26.0.24
'@types/node': 16.11.17
'@types/jest': 27.4.0
'@types/node': 16.11.19
'@types/react': 17.0.27
'@types/w3c-web-usb': 1.0.5
'@yume-chan/async': 2.1.4
bluebird: 3.7.2
clsx: 1.1.1
eslint: 7.32.0
eslint-config-next: 12.0.7_eslint@7.32.0+next@12.0.7
@ -3480,6 +3484,10 @@ packages:
'@babel/types': 7.16.0
dev: false
/@types/bluebird/3.5.36:
resolution: {integrity: sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==}
dev: false
/@types/body-parser/1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
@ -3603,11 +3611,11 @@ packages:
'@types/istanbul-lib-report': 3.0.0
dev: false
/@types/jest/26.0.24:
resolution: {integrity: sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==}
/@types/jest/27.4.0:
resolution: {integrity: sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==}
dependencies:
jest-diff: 26.6.2
pretty-format: 26.6.2
jest-diff: 27.4.6
pretty-format: 27.4.6
dev: false
/@types/json-schema/7.0.9:
@ -3643,6 +3651,10 @@ packages:
resolution: {integrity: sha512-C1vTZME8cFo8uxY2ui41xcynEotVkczIVI5AjLmy5pkpBv/FtG+jhtOlfcPysI8VRVwoOMv6NJm44LGnoMSWkw==}
dev: false
/@types/node/16.11.19:
resolution: {integrity: sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==}
dev: false
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
dev: false
@ -4204,6 +4216,11 @@ packages:
color-convert: 2.0.1
dev: false
/ansi-styles/5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
dev: false
/anymatch/2.0.0:
resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==}
dependencies:
@ -5981,6 +5998,11 @@ packages:
engines: {node: '>= 10.14.2'}
dev: false
/diff-sequences/27.4.0:
resolution: {integrity: sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dev: false
/diffie-hellman/5.0.3:
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
dependencies:
@ -8556,6 +8578,16 @@ packages:
pretty-format: 26.6.2
dev: false
/jest-diff/27.4.6:
resolution: {integrity: sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
chalk: 4.1.2
diff-sequences: 27.4.0
jest-get-type: 27.4.0
pretty-format: 27.4.6
dev: false
/jest-docblock/26.0.0:
resolution: {integrity: sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==}
engines: {node: '>= 10.14.2'}
@ -8609,6 +8641,11 @@ packages:
engines: {node: '>= 10.14.2'}
dev: false
/jest-get-type/27.4.0:
resolution: {integrity: sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dev: false
/jest-haste-map/26.6.2:
resolution: {integrity: sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==}
engines: {node: '>= 10.14.2'}
@ -10909,6 +10946,15 @@ packages:
react-is: 17.0.2
dev: false
/pretty-format/27.4.6:
resolution: {integrity: sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
ansi-regex: 5.0.1
ansi-styles: 5.2.0
react-is: 17.0.2
dev: false
/pretty-time/1.1.0:
resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==}
engines: {node: '>=4'}
@ -13991,11 +14037,12 @@ packages:
dev: false
file:projects/scrcpy.tgz:
resolution: {integrity: sha512-RhLyxPrPiFxJQ+Baq50eIebhA5txN5f+h4hIfFMcXQZzHFx7+HcdyZvUlDFS8Z5JngkwVGu5dD6lk59F78cCYA==, tarball: file:projects/scrcpy.tgz}
resolution: {integrity: sha512-ZoKLRmeOhzbvUl1tXGFjRmPKRFcGWQjkfocPpSRiZLvU0kdb62Plq65M2m0tnQuSsge+m1VcsbC7zf0fyMNmhQ==, tarball: file:projects/scrcpy.tgz}
name: '@rush-temp/scrcpy'
version: 0.0.0
dependencies:
'@types/dom-webcodecs': 0.1.3
'@types/jest': 27.4.0
'@yume-chan/async': 2.1.4
gh-release-fetch: 2.0.4
jest: 26.6.3
@ -14013,11 +14060,14 @@ packages:
dev: false
file:projects/struct.tgz:
resolution: {integrity: sha512-j1oCxwpkmde9LO5iyZHD3ndKGeD8ECVh7n8wpm4mqiQmHzay++2ky0djoqL5j3+sMycUmQfMKq7llNvNN0tDHg==, tarball: file:projects/struct.tgz}
resolution: {integrity: sha512-bQp42RvxKuE6ql0/uinMby4wEgAWnpgnWrtd2+Z3QD+SSTLIc3HpoXuuahoeWnx6rIWjS3zYgA5FKijJZflyGQ==, tarball: file:projects/struct.tgz}
name: '@rush-temp/struct'
version: 0.0.0
dependencies:
'@types/node': 16.11.17
'@types/bluebird': 3.5.36
'@types/jest': 27.4.0
'@types/node': 16.11.19
bluebird: 3.7.2
jest: 26.6.3
tslib: 2.3.1
typescript: 4.5.4
@ -14030,12 +14080,12 @@ packages:
dev: false
file:projects/ts-package-builder.tgz:
resolution: {integrity: sha512-M7CMjBTO+fqKzrmWZgx3veL5IqvlOvvjYRJsMe6l/ylsaApwrJthKN7QwHE1wcHm7M/cCbakhA8oucRMLSAqxw==, tarball: file:projects/ts-package-builder.tgz}
resolution: {integrity: sha512-EKE72WCmD/Gkx/d5IU0i1BApSo+DjvxTn9WyWeiq8VkVYw9MyXceD4Zio6Cntm7z2QHhaGkLkuCWhC0/h9XkQg==, tarball: file:projects/ts-package-builder.tgz}
name: '@rush-temp/ts-package-builder'
version: 0.0.0
dependencies:
'@types/jest': 26.0.24
'@types/node': 16.11.17
'@types/jest': 27.4.0
'@types/node': 16.11.19
json5: 2.2.0
typescript: 4.5.4
dev: false

View file

@ -8,6 +8,8 @@
A C-style structure serializer and deserializer. Written in TypeScript and highly takes advantage of its type system.
**WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
## Installation
```sh
@ -214,9 +216,9 @@ So it's technically possible to pass in an incompatible type (e.g. `string`). Bu
const value = await struct.deserialize(stream);
value.foo; // number
struct.serialize({ }, context) // error: 'foo' is required
struct.serialize({ foo: 'bar' }, context) // error: 'foo' must be a number
struct.serialize({ foo: 42 }, context) // ok
struct.serialize({ }) // error: 'foo' is required
struct.serialize({ foo: 'bar' }) // error: 'foo' must be a number
struct.serialize({ foo: 42 }) // ok
```
2. Set fields' type (can be used with [`placeholder` method](#placeholder))
@ -235,9 +237,9 @@ So it's technically possible to pass in an incompatible type (e.g. `string`). Bu
value.foo; // MyEnum
value.bar; // MyEnum.a
struct.serialize({ foo: 42, bar: MyEnum.a }, context); // error: 'foo' must be of type `MyEnum`
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }, context); // error: 'bar' must be of type `MyEnum.a`
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }, context); // ok
struct.serialize({ foo: 42, bar: MyEnum.a }); // error: 'foo' must be of type `MyEnum`
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // error: 'bar' must be of type `MyEnum.a`
struct.serialize({ foo: MyEnum.a, bar: MyEnum.b }); // ok
```
#### `int64`/`uint64`
@ -334,7 +336,7 @@ Merges (flats) another `Struct`'s fields and extra fields into the current one.
.fields(MyStructV1)
.int32('field2');
const structV2 = await MyStructV2.deserialize(context);
const structV2 = await MyStructV2.deserialize(stream);
structV2.field1; // number
structV2.field2; // number
// Fields are flatten
@ -352,7 +354,7 @@ Merges (flats) another `Struct`'s fields and extra fields into the current one.
.int32('field2')
.fields(MyStructV1);
const structV2 = await MyStructV2.deserialize(context);
const structV2 = await MyStructV2.deserialize(stream);
structV2.field1; // number
structV2.field2; // number
// Same result as above, but serialize/deserialize order is reversed
@ -409,8 +411,8 @@ Multiple calls merge all extra fields together.
value.foo; // number
value.bar; // 'hello'
struct.serialize({ foo: 42 }, context); // ok
struct.serialize({ foo: 42, bar: 'hello' }, context); // error: 'bar' is redundant
struct.serialize({ foo: 42 }); // ok
struct.serialize({ foo: 42, bar: 'hello' }); // error: 'bar' is redundant
```
2. Add getters and methods. `this` in functions refers to the result object.
@ -520,37 +522,43 @@ A callback returning anything other than `undefined` will cause `deserialize` to
#### `deserialize`
```ts
interface StructDeserializationContext {
decodeUtf8(buffer: ArrayBuffer): string;
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
interface StructDeserializeStream {
/**
* Read data from the underlying data source.
*
* Stream must return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it must throw an error.
*/
read(length: number): ArrayBuffer;
}
deserialize(
context: StructDeserializationContext
stream: StructDeserializeStream,
): TPostDeserialized extends undefined
? Overwrite<TExtra, TValue>
: TPostDeserialized
>;
deserialize(
stream: StructAsyncDeserializeStream,
): Promise<
TPostDeserialized extends undefined
? Overwrite<TExtra, TValue>
: TPostDeserialized
>
>;
```
Deserialize a Struct value from `context`.
Deserialize a Struct value from `stream`.
As the signature shows, if the `postDeserialize` callback returns any value, `deserialize` will return that value instead.
The `read` method of `context`, when being called, should returns exactly `length` bytes of data (or throw an `Error` if it can't).
The `read` method of `stream`, when being called, should returns exactly `length` bytes of data (or throw an `Error` if it can't).
#### `serialize`
```ts
interface StructSerializationContext {
encodeUtf8(input: string): ArrayBuffer;
}
serialize(
init: Omit<TFields, TOmitInitKey>,
context: StructSerializationContext
init: Omit<TFields, TOmitInitKey>
): ArrayBuffer;
```
@ -634,7 +642,6 @@ Actual size should be returned from `StructFieldValue#getSize`
```ts
abstract create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TValue,
): StructFieldValue<this>;
@ -649,16 +656,21 @@ Derived classes must implement this method to create its own field value instanc
```ts
abstract deserialize(
options: Readonly<StructOptions>,
context: StructDeserializationContext,
stream: StructDeserializeStream,
struct: StructValue,
): ValueOrPromise<StructFieldValue<this>>;
): StructFieldValue<this>;
abstract deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<StructFieldValue<this>>;
```
Derived classes must implement this method to define how to deserialize a value from `context`. Can also return a `Promise`.
Derived classes must implement this method to define how to deserialize a value from `stream`. Can also return a `Promise`.
Usually implementations should be:
1. Somehow parse the value from `context`
1. Somehow parse the value from `stream`
2. Pass the value into its `create` method
Sometimes, some metadata is present when deserializing, but need to be calculated when serializing, for example a UTF-8 encoded string may have different length between itself (character count) and serialized form (byte length). So `deserialize` can save those metadata on the `StructFieldValue` instance for later use.
@ -701,8 +713,7 @@ If one needs to manipulate other states when getting/setting values, they can ov
```ts
abstract serialize(
dataView: DataView,
offset: number,
context: StructSerializationContext
offset: number
): void;
```

View file

@ -33,12 +33,15 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"bluebird": "^3.7.2",
"tslib": "^2.3.1"
},
"devDependencies": {
"jest": "^26.6.3",
"typescript": "^4.4.3",
"@yume-chan/ts-package-builder": "^1.0.0",
"@types/node": "^16.9.1"
"@types/jest": "^27.4.0",
"@types/node": "^16.11.19",
"@types/bluebird": "^3.5.36"
}
}

View file

@ -1,30 +1,14 @@
import type { ValueOrPromise } from '../utils';
export interface StructDeserializeStream {
/**
* Context with enough methods to serialize a struct
* Read data from the underlying data source.
*
* Stream must return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it must throw an error.
*/
export interface StructSerializationContext {
/**
* Encode the specified string into an ArrayBuffer using UTF-8 encoding
*/
encodeUtf8(input: string): ArrayBuffer;
}
export interface StructSyncDeserializationContext extends StructSerializationContext {
decodeUtf8(buffer: ArrayBuffer): string;
read(length: number): 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;
export interface StructAsyncDeserializeStream {
/**
* Read data from the underlying data source.
*

View file

@ -1,5 +1,5 @@
import type { ValueOrPromise } from '../utils';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
import { StructFieldDefinition } from './definition';
import type { StructFieldValue } from './field-value';
import type { StructValue } from './struct-value';
@ -14,10 +14,24 @@ describe('StructFieldDefinition', () => {
public getSize(): number {
throw new Error('Method not implemented.');
}
public create(options: Readonly<StructOptions>, context: StructSerializationContext, struct: StructValue, value: unknown): StructFieldValue<this> {
public create(options: Readonly<StructOptions>, struct: StructValue, value: unknown): StructFieldValue<this> {
throw new Error('Method not implemented.');
}
public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, struct: StructValue): ValueOrPromise<StructFieldValue<this>> {
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream,
struct: StructValue,
): StructFieldValue<this>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<StructFieldValue<this>>;
public deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream | StructAsyncDeserializeStream,
struct: StructValue
): ValueOrPromise<StructFieldValue<this>> {
throw new Error('Method not implemented.');
}
}

View file

@ -1,5 +1,4 @@
import type { ValueOrPromise } from '../utils';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
import type { StructFieldValue } from './field-value';
import type { StructValue } from './struct-value';
@ -53,7 +52,6 @@ export abstract class StructFieldDefinition<
*/
public abstract create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TValue,
): StructFieldValue<this>;
@ -63,7 +61,12 @@ export abstract class StructFieldDefinition<
*/
public abstract deserialize(
options: Readonly<StructOptions>,
context: StructDeserializationContext,
stream: StructDeserializeStream,
struct: StructValue,
): ValueOrPromise<StructFieldValue<this>>;
): StructFieldValue<this>;
public abstract deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<StructFieldValue<this>>;
}

View file

@ -1,5 +1,5 @@
import type { ValueOrPromise } from '../utils';
import type { StructDeserializationContext, StructOptions, StructSerializationContext } from './context';
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructOptions } from './context';
import { StructFieldDefinition } from './definition';
import { StructFieldValue } from './field-value';
import type { StructValue } from './struct-value';
@ -8,21 +8,19 @@ describe('StructFieldValue', () => {
describe('.constructor', () => {
it('should save parameters', () => {
class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
public serialize(dataView: DataView, offset: number): void {
throw new Error('Method not implemented.');
}
}
const definition = 1 as any;
const options = 2 as any;
const context = 3 as any;
const struct = 4 as any;
const value = 5 as any;
const definition = {} as any;
const options = {} as any;
const struct = {} as any;
const value = {} as any;
const fieldValue = new MockStructFieldValue(definition, options, context, struct, value);
const fieldValue = new MockStructFieldValue(definition, options, struct, value);
expect(fieldValue).toHaveProperty('definition', definition);
expect(fieldValue).toHaveProperty('options', options);
expect(fieldValue).toHaveProperty('context', context);
expect(fieldValue).toHaveProperty('struct', struct);
expect(fieldValue.get()).toBe(value);
});
@ -34,22 +32,37 @@ describe('StructFieldValue', () => {
public getSize(): number {
return 42;
}
public create(options: Readonly<StructOptions>, context: StructSerializationContext, struct: StructValue, value: unknown): StructFieldValue<this> {
public create(options: Readonly<StructOptions>, struct: StructValue, value: unknown): StructFieldValue<this> {
throw new Error('Method not implemented.');
}
public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, struct: StructValue): ValueOrPromise<StructFieldValue<this>> {
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream,
struct: StructValue,
): StructFieldValue<this>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<StructFieldValue<this>>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream | StructAsyncDeserializeStream,
struct: StructValue,
): ValueOrPromise<StructFieldValue<this>> {
throw new Error('Method not implemented.');
}
}
class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
public serialize(dataView: DataView, offset: number): void {
throw new Error('Method not implemented.');
}
}
const fieldDefinition = new MockFieldDefinition();
const fieldValue = new MockStructFieldValue(fieldDefinition, undefined as any, undefined as any, undefined as any, undefined as any);
const fieldValue = new MockStructFieldValue(fieldDefinition, undefined as any, undefined as any, undefined as any);
expect(fieldValue.getSize()).toBe(42);
});
});
@ -57,12 +70,12 @@ describe('StructFieldValue', () => {
describe('#set', () => {
it('should update its internal value', () => {
class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
public serialize(dataView: DataView, offset: number): void {
throw new Error('Method not implemented.');
}
}
const fieldValue = new MockStructFieldValue(undefined as any, undefined as any, undefined as any, undefined as any, undefined as any);
const fieldValue = new MockStructFieldValue(undefined as any, undefined as any, undefined as any, undefined as any);
fieldValue.set(1);
expect(fieldValue.get()).toBe(1);

View file

@ -1,4 +1,4 @@
import type { StructOptions, StructSerializationContext } from './context';
import type { StructOptions } from './context';
import type { StructFieldDefinition } from './definition';
import type { StructValue } from './struct-value';
@ -17,9 +17,6 @@ export abstract class StructFieldValue<
/** Gets the options of the associated `Struct` */
public readonly options: Readonly<StructOptions>;
/** Gets the serialization context of the associated `Struct` instance */
public readonly context: StructSerializationContext;
/** Gets the associated `Struct` instance */
public readonly struct: StructValue;
@ -28,13 +25,11 @@ export abstract class StructFieldValue<
public constructor(
definition: TDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TDefinition['TValue'],
) {
this.definition = definition;
this.options = options;
this.context = context;
this.struct = struct;
this.value = value;
}
@ -68,6 +63,5 @@ export abstract class StructFieldValue<
public abstract serialize(
dataView: DataView,
offset: number,
context: StructSerializationContext
): void;
}

View file

@ -1,16 +1,12 @@
import { StructDefaultOptions, StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from './basic';
import { StructAsyncDeserializeStream, StructDefaultOptions, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from './basic';
import { Struct } from './struct';
import { ArrayBufferFieldType, FixedLengthArrayBufferLikeFieldDefinition, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition } from './types';
import { ValueOrPromise } from './utils';
class MockDeserializationContext implements StructDeserializationContext {
class MockDeserializationStream implements StructDeserializeStream {
public buffer = new ArrayBuffer(0);
public read = jest.fn((length: number) => this.buffer);
public encodeUtf8 = jest.fn((input: string) => Buffer.from(input, 'utf-8'));
public decodeUtf8 = jest.fn((buffer: ArrayBuffer) => Buffer.from(buffer).toString('utf-8'));
}
describe('Struct', () => {
@ -32,10 +28,24 @@ describe('Struct', () => {
return this.options;
});
public create(options: Readonly<StructOptions>, context: StructSerializationContext, struct: StructValue, value: unknown): StructFieldValue<this> {
public create(options: Readonly<StructOptions>, struct: StructValue, value: unknown): StructFieldValue<this> {
throw new Error('Method not implemented.');
}
public deserialize(options: Readonly<StructOptions>, context: StructDeserializationContext, struct: StructValue): ValueOrPromise<StructFieldValue<this>> {
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream,
struct: StructValue,
): StructFieldValue<this>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<StructFieldValue<this>>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream | StructAsyncDeserializeStream,
struct: StructValue
): ValueOrPromise<StructFieldValue<this>> {
throw new Error('Method not implemented.');
}
}
@ -265,17 +275,17 @@ describe('Struct', () => {
.int8('foo')
.int16('bar');
const context = new MockDeserializationContext();
context.read
const stream = new MockDeserializationStream();
stream.read
.mockReturnValueOnce(new Uint8Array([2]).buffer)
.mockReturnValueOnce(new Uint8Array([0, 16]).buffer);
const result = await struct.deserialize(context);
const result = await struct.deserialize(stream);
expect(result).toEqual({ foo: 2, bar: 16 });
expect(context.read).toBeCalledTimes(2);
expect(context.read).nthCalledWith(1, 1);
expect(context.read).nthCalledWith(2, 2);
expect(stream.read).toBeCalledTimes(2);
expect(stream.read).nthCalledWith(1, 1);
expect(stream.read).nthCalledWith(2, 2);
});
it('should deserialize with dynamic size fields', async () => {
@ -283,16 +293,16 @@ describe('Struct', () => {
.int8('fooLength')
.uint8ClampedArray('foo', { lengthField: 'fooLength' });
const context = new MockDeserializationContext();
context.read
const stream = new MockDeserializationStream();
stream.read
.mockReturnValueOnce(new Uint8Array([2]).buffer)
.mockReturnValueOnce(new Uint8Array([3, 4]).buffer);
const result = await struct.deserialize(context);
const result = await struct.deserialize(stream);
expect(result).toEqual({ fooLength: 2, foo: new Uint8ClampedArray([3, 4]) });
expect(context.read).toBeCalledTimes(2);
expect(context.read).nthCalledWith(1, 1);
expect(context.read).nthCalledWith(2, 2);
expect(stream.read).toBeCalledTimes(2);
expect(stream.read).nthCalledWith(1, 1);
expect(stream.read).nthCalledWith(2, 2);
});
});
@ -301,8 +311,8 @@ describe('Struct', () => {
const struct = new Struct()
.extra({ foo: 42, bar: true });
const context = new MockDeserializationContext();
const result = await struct.deserialize(context);
const stream = new MockDeserializationStream();
const result = await struct.deserialize(stream);
expect(Object.entries(Object.getOwnPropertyDescriptors(result))).toEqual([
['foo', { configurable: true, enumerable: true, writable: true, value: 42 }],
@ -318,8 +328,8 @@ describe('Struct', () => {
set bar(value) { },
});
const context = new MockDeserializationContext();
const result = await struct.deserialize(context);
const stream = new MockDeserializationStream();
const result = await struct.deserialize(stream);
expect(Object.entries(Object.getOwnPropertyDescriptors(result))).toEqual([
['foo', { configurable: true, enumerable: true, get: expect.any(Function) }],
@ -334,8 +344,8 @@ describe('Struct', () => {
const callback = jest.fn(() => { throw new Error('mock'); });
struct.postDeserialize(callback);
const context = new MockDeserializationContext();
expect(struct.deserialize(context)).rejects.toThrowError('mock');
const stream = new MockDeserializationStream();
expect(struct.deserialize(stream)).rejects.toThrowError('mock');
expect(callback).toBeCalledTimes(1);
});
@ -344,8 +354,8 @@ describe('Struct', () => {
const callback = jest.fn(() => 'mock');
struct.postDeserialize(callback);
const context = new MockDeserializationContext();
expect(struct.deserialize(context)).resolves.toBe('mock');
const stream = new MockDeserializationStream();
expect(struct.deserialize(stream)).resolves.toBe('mock');
expect(callback).toBeCalledTimes(1);
expect(callback).toBeCalledWith({});
});
@ -355,8 +365,8 @@ describe('Struct', () => {
const callback = jest.fn();
struct.postDeserialize(callback);
const context = new MockDeserializationContext();
const result = await struct.deserialize(context);
const stream = new MockDeserializationStream();
const result = await struct.deserialize(stream);
expect(callback).toBeCalledTimes(1);
expect(callback).toBeCalledWith(result);
@ -371,8 +381,8 @@ describe('Struct', () => {
const callback2 = jest.fn();
struct.postDeserialize(callback2);
const context = new MockDeserializationContext();
await struct.deserialize(context);
const stream = new MockDeserializationStream();
await struct.deserialize(stream);
expect(callback1).toBeCalledTimes(0);
expect(callback2).toBeCalledTimes(1);
@ -386,8 +396,8 @@ describe('Struct', () => {
.int8('foo')
.int16('bar');
const context = new MockDeserializationContext();
const result = new Uint8Array(struct.serialize({ foo: 0x42, bar: 0x1024 }, context));
const stream = new MockDeserializationStream();
const result = new Uint8Array(struct.serialize({ foo: 0x42, bar: 0x1024 }));
expect(result).toEqual(new Uint8Array([0x42, 0x10, 0x24]));
});
@ -397,8 +407,8 @@ describe('Struct', () => {
.int8('fooLength')
.arrayBuffer('foo', { lengthField: 'fooLength' });
const context = new MockDeserializationContext();
const result = new Uint8Array(struct.serialize({ foo: new Uint8Array([0x03, 0x04, 0x05]).buffer }, context));
const stream = new MockDeserializationStream();
const result = new Uint8Array(struct.serialize({ foo: new Uint8Array([0x03, 0x04, 0x05]).buffer }));
expect(result).toEqual(new Uint8Array([0x03, 0x03, 0x04, 0x05]));
});

View file

@ -1,9 +1,11 @@
import { StructDefaultOptions, StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from './basic';
import type { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions } from './basic';
import { StructDefaultOptions, StructValue } from './basic';
import { Syncbird } from "./syncbird";
import { ArrayBufferFieldType, ArrayBufferLikeFieldType, FixedLengthArrayBufferLikeFieldDefinition, FixedLengthArrayBufferLikeFieldOptions, LengthField, NumberFieldDefinition, NumberFieldType, StringFieldType, Uint8ClampedArrayFieldType, VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldOptions } from './types';
import { Awaited, Evaluate, Identity, Overwrite } from './utils';
import { Awaited, Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils";
export interface StructLike<TValue> {
deserialize(context: StructDeserializationContext): Promise<TValue>;
deserialize(stream: StructDeserializeStream | StructAsyncDeserializeStream): Promise<TValue>;
}
/**
@ -53,7 +55,7 @@ interface ArrayBufferLikeFieldCreator<
*/
<
TName extends PropertyKey,
TType extends ArrayBufferLikeFieldType,
TType extends ArrayBufferLikeFieldType<any, any>,
TTypeScriptType = TType['TTypeScriptType'],
>(
name: TName,
@ -77,7 +79,7 @@ interface ArrayBufferLikeFieldCreator<
*/
<
TName extends PropertyKey,
TType extends ArrayBufferLikeFieldType,
TType extends ArrayBufferLikeFieldType<any, any>,
TOptions extends VariableLengthArrayBufferLikeFieldOptions<TFields>,
TTypeScriptType = TType['TTypeScriptType'],
>(
@ -106,7 +108,7 @@ interface BindedArrayBufferLikeFieldDefinitionCreator<
TOmitInitKey extends PropertyKey,
TExtra extends object,
TPostDeserialized,
TType extends ArrayBufferLikeFieldType
TType extends ArrayBufferLikeFieldType<any, any>
> {
<
TName extends PropertyKey,
@ -520,17 +522,36 @@ export class Struct<
return this as any;
}
public async deserialize(
context: StructDeserializationContext
): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> {
public deserialize(
stream: StructDeserializeStream,
): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
public deserialize(
stream: StructAsyncDeserializeStream,
): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>;
public deserialize(
stream: StructDeserializeStream | StructAsyncDeserializeStream,
): ValueOrPromise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> {
const value = new StructValue();
Object.defineProperties(value.value, this._extra);
for (const [name, definition] of this._fields) {
const fieldValue = await definition.deserialize(this.options, context, value);
value.set(name, fieldValue);
return Syncbird.try(() => {
const iterator = this._fields[Symbol.iterator]();
const iterate: () => StructValue | Syncbird<StructValue> = () => {
const result = iterator.next();
if (result.done) {
return value;
}
const [name, definition] = result.value;
return Syncbird.resolve(
definition.deserialize(this.options, stream as any, value)
).then(fieldValue => {
value.set(name, fieldValue);
return iterate();
});
};
return iterate();
}).then(value => {
if (this._postDeserialized) {
const object = value.value as TFields;
const result = this._postDeserialized.call(object, object);
@ -539,14 +560,15 @@ export class Struct<
}
}
return value.value as any;
return value.value;
}).valueOrPromise();
}
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>, context: StructSerializationContext): ArrayBuffer {
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): ArrayBuffer {
const value = new StructValue();
for (const [name, definition] of this._fields) {
const fieldValue = definition.create(this.options, context, value, (init as any)[name]);
const fieldValue = definition.create(this.options, value, (init as any)[name]);
value.set(name, fieldValue);
}
@ -564,7 +586,7 @@ export class Struct<
const dataView = new DataView(buffer);
let offset = 0;
for (const { fieldValue, size } of fieldsInfo) {
fieldValue.serialize(dataView, offset, context);
fieldValue.serialize(dataView, offset);
offset += size;
}

View file

@ -0,0 +1,40 @@
import Bluebird from 'bluebird';
export type Resolvable<R> = R | PromiseLike<R>;
export class Syncbird<T> extends Bluebird<T> implements PromiseLike<T> {
public static resolve(): Syncbird<void>;
public static resolve<R>(value: Resolvable<R>): Syncbird<R>;
public static resolve<R>(value?: Resolvable<R>): Syncbird<R> {
return new Syncbird(
resolve => resolve(value)
);
}
static try<R>(fn: () => Resolvable<R>): Syncbird<R> {
return Syncbird.resolve(fn());
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Syncbird<TResult1 | TResult2> {
if (this.isFulfilled()) {
if (!onfulfilled) {
return this as unknown as Syncbird<TResult1>;
} else {
return Syncbird.resolve(onfulfilled(this.value()));
}
} else {
return Syncbird.resolve(super.then(onfulfilled, onrejected));
}
}
public valueOrPromise(): T | Promise<T> {
if (this.isFulfilled()) {
return this.value();
} else {
return this as Promise<T>;
}
}
}

View file

@ -1,14 +1,10 @@
import { StructDefaultOptions, StructDeserializationContext, StructValue } from '../basic';
import { StructDefaultOptions, StructDeserializeStream, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, StringFieldType, Uint8ClampedArrayFieldType } from './array-buffer';
class MockDeserializationContext implements StructDeserializationContext {
class MockDeserializationStream implements StructDeserializeStream {
public buffer = new ArrayBuffer(0);
public read = jest.fn((length: number) => this.buffer);
public encodeUtf8 = jest.fn((input: string) => Buffer.from(input, 'utf-8'));
public decodeUtf8 = jest.fn((buffer: ArrayBuffer) => Buffer.from(buffer).toString('utf-8'));
}
describe('Types', () => {
@ -67,27 +63,13 @@ describe('Types', () => {
it('`#toArrayBuffer` should return the decoded string', () => {
const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8');
const context = new MockDeserializationContext();
expect(StringFieldType.instance.toArrayBuffer(text, context)).toEqual(arrayBuffer);
expect(context.encodeUtf8).toBeCalledTimes(1);
expect(context.encodeUtf8).toBeCalledWith(text);
expect(StringFieldType.instance.toArrayBuffer(text)).toEqual(arrayBuffer);
});
it('`#fromArrayBuffer` should return the encoded ArrayBuffer', () => {
const text = 'foo';
const arrayBuffer = Buffer.from(text, 'utf-8');
const context: StructDeserializationContext = {
decodeUtf8(arrayBuffer: ArrayBuffer): string {
return Buffer.from(arrayBuffer).toString('utf-8');
},
encodeUtf8(input) {
throw new Error('Method not implemented.');
},
read(length) {
throw new Error('Method not implemented.');
},
};
expect(StringFieldType.instance.fromArrayBuffer(arrayBuffer, context)).toBe(text);
expect(StringFieldType.instance.fromArrayBuffer(arrayBuffer)).toBe(text);
});
it('`#getSize` should return -1', () => {
@ -107,7 +89,7 @@ describe('Types', () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
const context = new MockDeserializationStream();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
@ -124,7 +106,7 @@ describe('Types', () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(Uint8ClampedArrayFieldType.instance, size);
const context = new MockDeserializationContext();
const context = new MockDeserializationStream();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
@ -143,7 +125,7 @@ describe('Types', () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
const context = new MockDeserializationStream();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
@ -165,7 +147,7 @@ describe('Types', () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
const context = new MockDeserializationStream();
const buffer = new ArrayBuffer(size);
context.buffer = buffer;
const struct = new StructValue();
@ -184,7 +166,7 @@ describe('Types', () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
const context = new MockDeserializationStream();
const sourceArray = new Uint8Array(Array.from({ length: size }, (_, i) => i));
const buffer = sourceArray.buffer;
context.buffer = buffer;
@ -194,7 +176,7 @@ describe('Types', () => {
const targetArray = new Uint8Array(size);
const targetView = new DataView(targetArray.buffer);
fieldValue.serialize(targetView, 0, context);
fieldValue.serialize(targetView, 0);
expect(targetArray).toEqual(sourceArray);
});
@ -203,7 +185,7 @@ describe('Types', () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(ArrayBufferFieldType.instance, size);
const context = new MockDeserializationContext();
const context = new MockDeserializationStream();
const sourceArray = new Uint8Array(Array.from({ length: size }, (_, i) => i));
const buffer = sourceArray.buffer;
context.buffer = buffer;
@ -215,7 +197,7 @@ describe('Types', () => {
const targetArray = new Uint8Array(size);
const targetView = new DataView(targetArray.buffer);
fieldValue.serialize(targetView, 0, context);
fieldValue.serialize(targetView, 0);
expect(targetArray).toEqual(sourceArray);
});

View file

@ -1,4 +1,6 @@
import { StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from '../basic';
import { Syncbird } from "../syncbird";
import { decodeUtf8, encodeUtf8, ValueOrPromise } from "../utils";
/**
* Base class for all types that
@ -18,10 +20,10 @@ export abstract class ArrayBufferLikeFieldType<TValue = unknown, TTypeScriptType
* This function should be "pure", i.e.,
* same `value` should always be converted to `ArrayBuffer`s that have same content.
*/
public abstract toArrayBuffer(value: TValue, context: StructSerializationContext): ArrayBuffer;
public abstract toArrayBuffer(value: TValue): ArrayBuffer;
/** When implemented in derived classes, converts the `ArrayBuffer` to a type-specific value */
public abstract fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): TValue;
public abstract fromArrayBuffer(arrayBuffer: ArrayBuffer): TValue;
/**
* When implemented in derived classes, gets the size in byte of the type-specific `value`.
@ -76,17 +78,17 @@ export class Uint8ClampedArrayFieldType
}
}
/** Am ArrayBufferLike type that converts between `ArrayBuffer` and `string` */
/** An ArrayBufferLike type that converts between `ArrayBuffer` and `string` */
export class StringFieldType<TTypeScriptType = string>
extends ArrayBufferLikeFieldType<string, TTypeScriptType> {
public static readonly instance = new StringFieldType();
public toArrayBuffer(value: string, context: StructSerializationContext): ArrayBuffer {
return context.encodeUtf8(value);
public toArrayBuffer(value: string): ArrayBuffer {
return encodeUtf8(value);
}
public fromArrayBuffer(arrayBuffer: ArrayBuffer, context: StructDeserializationContext): string {
return context.decodeUtf8(arrayBuffer);
public fromArrayBuffer(arrayBuffer: ArrayBuffer): string {
return decodeUtf8(arrayBuffer);
}
public getSize(): number {
@ -100,7 +102,7 @@ export class StringFieldType<TTypeScriptType = string>
const EmptyArrayBuffer = new ArrayBuffer(0);
export abstract class ArrayBufferLikeFieldDefinition<
TType extends ArrayBufferLikeFieldType = ArrayBufferLikeFieldType,
TType extends ArrayBufferLikeFieldType<any, any> = ArrayBufferLikeFieldType<unknown, unknown>,
TOptions = void,
TOmitInitKey extends PropertyKey = never,
> extends StructFieldDefinition<
@ -124,47 +126,55 @@ export abstract class ArrayBufferLikeFieldDefinition<
*/
public create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TType['TTypeScriptType'],
arrayBuffer?: ArrayBuffer,
): ArrayBufferLikeFieldValue<this> {
return new ArrayBufferLikeFieldValue(this, options, context, struct, value, arrayBuffer);
return new ArrayBufferLikeFieldValue(this, options, struct, value, arrayBuffer);
}
public async deserialize(
public override deserialize(
options: Readonly<StructOptions>,
context: StructDeserializationContext,
stream: StructDeserializeStream,
struct: StructValue,
): Promise<ArrayBufferLikeFieldValue<this>> {
): ArrayBufferLikeFieldValue<this>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<ArrayBufferLikeFieldValue<this>>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream | StructAsyncDeserializeStream,
struct: StructValue,
): ValueOrPromise<ArrayBufferLikeFieldValue<this>> {
return Syncbird.resolve().then(() => {
const size = this.getDeserializeSize(struct);
let arrayBuffer: ArrayBuffer;
if (size === 0) {
arrayBuffer = EmptyArrayBuffer;
return EmptyArrayBuffer;
} else {
arrayBuffer = await context.read(size);
return stream.read(size);
}
const value = this.type.fromArrayBuffer(arrayBuffer, context);
return this.create(options, context, struct, value, arrayBuffer);
}).then(arrayBuffer => {
const value = this.type.fromArrayBuffer(arrayBuffer);
return this.create(options, struct, value, arrayBuffer);
}).valueOrPromise();
}
}
export class ArrayBufferLikeFieldValue<
TDefinition extends ArrayBufferLikeFieldDefinition<ArrayBufferLikeFieldType, any, any>,
TDefinition extends ArrayBufferLikeFieldDefinition<ArrayBufferLikeFieldType<unknown, unknown>, any, any>,
> extends StructFieldValue<TDefinition> {
protected arrayBuffer: ArrayBuffer | undefined;
public constructor(
definition: TDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TDefinition['TValue'],
arrayBuffer?: ArrayBuffer,
) {
super(definition, options, context, struct, value);
super(definition, options, struct, value);
this.arrayBuffer = arrayBuffer;
}
@ -173,9 +183,9 @@ export class ArrayBufferLikeFieldValue<
this.arrayBuffer = undefined;
}
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
public serialize(dataView: DataView, offset: number): void {
if (!this.arrayBuffer) {
this.arrayBuffer = this.definition.type.toArrayBuffer(this.value, context);
this.arrayBuffer = this.definition.type.toArrayBuffer(this.value);
}
new Uint8Array(dataView.buffer)

View file

@ -1,4 +1,4 @@
import { StructDefaultOptions, StructDeserializationContext, StructSerializationContext, StructValue } from '../basic';
import { StructDefaultOptions, StructDeserializeStream, StructValue } from '../basic';
import { NumberFieldDefinition, NumberFieldType } from './number';
describe('Types', () => {
@ -78,17 +78,13 @@ describe('Types', () => {
describe('#deserialize', () => {
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 stream: StructDeserializeStream = { read };
const definition = new NumberFieldDefinition(NumberFieldType.Uint8);
const struct = new StructValue();
const value = await definition.deserialize(
StructDefaultOptions,
context,
stream,
struct,
);
@ -99,17 +95,13 @@ describe('Types', () => {
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 stream: StructDeserializeStream = { read };
const definition = new NumberFieldDefinition(NumberFieldType.Uint16);
const struct = new StructValue();
const value = await definition.deserialize(
StructDefaultOptions,
context,
stream,
struct,
);
@ -120,17 +112,13 @@ describe('Types', () => {
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 stream: StructDeserializeStream = { read };
const definition = new NumberFieldDefinition(NumberFieldType.Uint16);
const struct = new StructValue();
const value = await definition.deserialize(
{ ...StructDefaultOptions, littleEndian: true },
context,
stream,
struct,
);
@ -144,16 +132,12 @@ describe('Types', () => {
describe('NumberFieldValue', () => {
describe('#getSize', () => {
it('should return size of its definition', () => {
const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); },
};
const struct = new StructValue();
expect(
new NumberFieldDefinition(NumberFieldType.Int8)
.create(
StructDefaultOptions,
context,
struct,
42,
)
@ -164,7 +148,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Uint8)
.create(
StructDefaultOptions,
context,
struct,
42,
)
@ -175,7 +158,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Int16)
.create(
StructDefaultOptions,
context,
struct,
42,
)
@ -186,7 +168,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Uint16)
.create(
StructDefaultOptions,
context,
struct,
42,
)
@ -197,7 +178,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Int32)
.create(
StructDefaultOptions,
context,
struct,
42,
)
@ -208,7 +188,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Uint32)
.create(
StructDefaultOptions,
context,
struct,
42,
)
@ -219,7 +198,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Int64)
.create(
StructDefaultOptions,
context,
struct,
BigInt(100),
)
@ -230,7 +208,6 @@ describe('Types', () => {
new NumberFieldDefinition(NumberFieldType.Uint64)
.create(
StructDefaultOptions,
context,
struct,
BigInt(100),
)
@ -241,14 +218,10 @@ describe('Types', () => {
describe('#serialize', () => {
it('should serialize uint8', () => {
const context: StructSerializationContext = {
encodeUtf8(input) { throw new Error(''); },
};
const definition = new NumberFieldDefinition(NumberFieldType.Int8);
const struct = new StructValue();
const value = definition.create(
StructDefaultOptions,
context,
struct,
42,
);

View file

@ -1,4 +1,6 @@
import { StructDeserializationContext, StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { StructAsyncDeserializeStream, StructDeserializeStream, StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from '../basic';
import { Syncbird } from "../syncbird";
import { ValueOrPromise } from "../utils";
export type DataViewGetters =
{ [TKey in keyof DataView]: TKey extends `get${string}` ? TKey : never }[keyof DataView];
@ -62,25 +64,37 @@ export class NumberFieldDefinition<
public create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TTypeScriptType,
): NumberFieldValue<this> {
return new NumberFieldValue(this, options, context, struct, value);
return new NumberFieldValue(this, options, struct, value);
}
public async deserialize(
public override deserialize(
options: Readonly<StructOptions>,
context: StructDeserializationContext,
stream: StructDeserializeStream,
struct: StructValue,
): Promise<NumberFieldValue<this>> {
const buffer = await context.read(this.getSize());
): NumberFieldValue<this>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructAsyncDeserializeStream,
struct: StructValue,
): Promise<NumberFieldValue<this>>;
public override deserialize(
options: Readonly<StructOptions>,
stream: StructDeserializeStream | StructAsyncDeserializeStream,
struct: StructValue,
): ValueOrPromise<NumberFieldValue<this>> {
return Syncbird.try(() => {
return stream.read(this.getSize());
}).then(buffer => {
const view = new DataView(buffer);
const value = view[this.type.dataViewGetter](
0,
options.littleEndian
) as any;
return this.create(options, context, struct, value);
return this.create(options, struct, value);
}).valueOrPromise();
}
}

View file

@ -1,20 +1,10 @@
import { StructDefaultOptions, StructDeserializationContext, StructFieldValue, StructSerializationContext, StructValue } from '../basic';
import { StructDefaultOptions, StructFieldValue, StructValue } from '../basic';
import { ArrayBufferFieldType, ArrayBufferLikeFieldType } from './array-buffer';
import { VariableLengthArrayBufferLikeFieldDefinition, VariableLengthArrayBufferLikeFieldLengthValue, VariableLengthArrayBufferLikeStructFieldValue } from './variable-length-array-buffer';
class MockDeserializationContext implements StructDeserializationContext {
public buffer = new ArrayBuffer(0);
public read = jest.fn((length: number) => this.buffer);
public encodeUtf8 = jest.fn((input: string) => Buffer.from(input, 'utf-8'));
public decodeUtf8 = jest.fn((buffer: ArrayBuffer) => Buffer.from(buffer).toString('utf-8'));
}
class MockOriginalFieldValue extends StructFieldValue {
public constructor() {
super({} as any, {} as any, {} as any, {} as any, {});
super({} as any, {} as any, {} as any, {});
}
public value: string | number = 0;
@ -27,21 +17,21 @@ class MockOriginalFieldValue extends StructFieldValue {
public set = jest.fn((value: string | number) => { });
public serialize = jest.fn((dataView: DataView, offset: number, context: StructSerializationContext): void => { });
public serialize = jest.fn((dataView: DataView, offset: number): void => { });
}
describe('Types', () => {
describe('VariableLengthArrayBufferLikeFieldLengthValue', () => {
class MockArrayBufferFieldValue extends StructFieldValue {
public constructor() {
super({ options: {} } as any, {} as any, {} as any, {} as any, {});
super({ options: {} } as any, {} as any, {} as any, {});
}
public size = 0;
public getSize = jest.fn(() => this.size);
public serialize(dataView: DataView, offset: number, context: StructSerializationContext): void {
public serialize(dataView: DataView, offset: number): void {
throw new Error('Method not implemented.');
}
}
@ -141,26 +131,25 @@ describe('Types', () => {
let dataView = 0 as any;
let offset = 1 as any;
let context = 2 as any;
mockOriginalFieldValue.value = 10;
mockArrayBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, offset, context);
lengthFieldValue.serialize(dataView, offset);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toReturnWith(10);
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith(0);
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset);
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockArrayBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, offset, context);
lengthFieldValue.serialize(dataView, offset);
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith(100);
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset);
});
it('should stringify its length if `originalField` is a string', async () => {
@ -173,26 +162,25 @@ describe('Types', () => {
let dataView = 0 as any;
let offset = 1 as any;
let context = 2 as any;
mockOriginalFieldValue.value = '10';
mockArrayBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, offset, context);
lengthFieldValue.serialize(dataView, offset);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toReturnWith('10');
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith('0');
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset);
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockArrayBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, offset, context);
lengthFieldValue.serialize(dataView, offset);
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith('100');
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset);
});
it('should stringify its length in specified base if `originalField` is a string', async () => {
@ -208,26 +196,25 @@ describe('Types', () => {
let dataView = 0 as any;
let offset = 1 as any;
let context = 2 as any;
mockOriginalFieldValue.value = '10';
mockArrayBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, offset, context);
lengthFieldValue.serialize(dataView, offset);
expect(mockOriginalFieldValue.get).toBeCalledTimes(1);
expect(mockOriginalFieldValue.get).toReturnWith('10');
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith('0');
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset);
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockArrayBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, offset, context);
lengthFieldValue.serialize(dataView, offset);
expect(mockOriginalFieldValue.set).toBeCalledTimes(1);
expect(mockOriginalFieldValue.set).toBeCalledWith((100).toString(base));
expect(mockOriginalFieldValue.serialize).toBeCalledTimes(1);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset, context);
expect(mockOriginalFieldValue.serialize).toBeCalledWith(dataView, offset);
});
});
});
@ -246,20 +233,17 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(0);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
);
expect(arrayBufferFieldValue).toHaveProperty('definition', arrayBufferFieldDefinition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', undefined);
@ -278,13 +262,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
value,
@ -292,7 +274,6 @@ describe('Types', () => {
expect(arrayBufferFieldValue).toHaveProperty('definition', arrayBufferFieldDefinition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', value);
@ -311,13 +292,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(0);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
);
@ -329,11 +308,11 @@ describe('Types', () => {
describe('#getSize', () => {
class MockArrayBufferFieldType extends ArrayBufferLikeFieldType<ArrayBuffer> {
public toArrayBuffer = jest.fn((value: ArrayBuffer, context: StructSerializationContext): ArrayBuffer => {
public toArrayBuffer = jest.fn((value: ArrayBuffer): ArrayBuffer => {
return value;
});
public fromArrayBuffer = jest.fn((arrayBuffer: ArrayBuffer, context: StructDeserializationContext): ArrayBuffer => {
public fromArrayBuffer = jest.fn((arrayBuffer: ArrayBuffer): ArrayBuffer => {
return arrayBuffer;
});
@ -357,13 +336,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
value,
@ -388,13 +365,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
);
@ -421,13 +396,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
);
@ -455,13 +428,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
value,
@ -485,13 +456,11 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = new VariableLengthArrayBufferLikeStructFieldValue(
arrayBufferFieldDefinition,
StructDefaultOptions,
context,
struct,
value,
value,
@ -597,18 +566,15 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = definition.create(
StructDefaultOptions,
context,
struct,
value,
);
expect(arrayBufferFieldValue).toHaveProperty('definition', definition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', undefined);
@ -627,11 +593,9 @@ describe('Types', () => {
{ lengthField },
);
const context = new MockDeserializationContext();
const value = new ArrayBuffer(100);
const arrayBufferFieldValue = definition.create(
StructDefaultOptions,
context,
struct,
value,
value,
@ -639,7 +603,6 @@ describe('Types', () => {
expect(arrayBufferFieldValue).toHaveProperty('definition', definition);
expect(arrayBufferFieldValue).toHaveProperty('options', StructDefaultOptions);
expect(arrayBufferFieldValue).toHaveProperty('context', context);
expect(arrayBufferFieldValue).toHaveProperty('struct', struct);
expect(arrayBufferFieldValue).toHaveProperty('value', value);
expect(arrayBufferFieldValue).toHaveProperty('arrayBuffer', value);

View file

@ -1,4 +1,4 @@
import { StructFieldDefinition, StructFieldValue, StructOptions, StructSerializationContext, StructValue } from '../basic';
import { StructFieldDefinition, StructFieldValue, StructOptions, StructValue } from '../basic';
import type { KeysOfType } from '../utils';
import { ArrayBufferLikeFieldDefinition, ArrayBufferLikeFieldType, ArrayBufferLikeFieldValue } from './array-buffer';
@ -35,7 +35,6 @@ export class VariableLengthArrayBufferLikeFieldDefinition<
public create(
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TType['TTypeScriptType'],
arrayBuffer?: ArrayBuffer
@ -43,7 +42,6 @@ export class VariableLengthArrayBufferLikeFieldDefinition<
return new VariableLengthArrayBufferLikeStructFieldValue(
this,
options,
context,
struct,
value,
arrayBuffer,
@ -61,12 +59,11 @@ export class VariableLengthArrayBufferLikeStructFieldValue<
public constructor(
definition: TDefinition,
options: Readonly<StructOptions>,
context: StructSerializationContext,
struct: StructValue,
value: TDefinition['TValue'],
arrayBuffer?: ArrayBuffer,
) {
super(definition, options, context, struct, value, arrayBuffer);
super(definition, options, struct, value, arrayBuffer);
if (arrayBuffer) {
this.length = arrayBuffer.byteLength;
@ -87,7 +84,7 @@ export class VariableLengthArrayBufferLikeStructFieldValue<
if (this.length === undefined) {
this.length = this.definition.type.getSize(this.value);
if (this.length === -1) {
this.arrayBuffer = this.definition.type.toArrayBuffer(this.value, this.context);
this.arrayBuffer = this.definition.type.toArrayBuffer(this.value);
this.length = this.arrayBuffer.byteLength;
}
}
@ -116,7 +113,7 @@ export class VariableLengthArrayBufferLikeFieldLengthValue
originalField: StructFieldValue,
arrayBufferField: VariableLengthArrayBufferLikeFieldValueLike,
) {
super(originalField.definition, originalField.options, originalField.context, originalField.struct, 0);
super(originalField.definition, originalField.options, originalField.struct, 0);
this.originalField = originalField;
this.arrayBufferField = arrayBufferField;
}
@ -138,8 +135,8 @@ export class VariableLengthArrayBufferLikeFieldLengthValue
set() { }
serialize(dataView: DataView, offset: number, context: StructSerializationContext) {
serialize(dataView: DataView, offset: number) {
this.originalField.set(this.get());
this.originalField.serialize(dataView, offset, context);
this.originalField.serialize(dataView, offset);
}
}

View file

@ -49,3 +49,16 @@ export type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
export function placeholder<T>(): T {
return undefined as unknown as T;
}
// @ts-expect-error @types/node missing `TextEncoder`
const Utf8Encoder = new TextEncoder();
// @ts-expect-error @types/node missing `TextDecoder`
const Utf8Decoder = new TextDecoder();
export function encodeUtf8(input: string): ArrayBuffer {
return Utf8Encoder.encode(input).buffer;
}
export function decodeUtf8(buffer: ArrayBuffer): string {
return Utf8Decoder.decode(buffer);
}

View file

@ -3,8 +3,8 @@
"compilerOptions": {
"types": [
"node",
"jest"
]
"jest",
],
},
"testTypes": [
"node",

View file

@ -13,11 +13,11 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.9.1"
"@types/node": "^16.11.19"
},
"dependencies": {
"json5": "^2.2.0",
"@types/jest": "^26.0.23"
"@types/jest": "^27.4.0"
},
"peerDependencies": {
"typescript": "^4.0.0"