refactor: performance optimizations

also renamed some internal types, and adding more code comments. (not thoroughly tested yet)
This commit is contained in:
Simon Chan 2024-05-04 23:47:52 +08:00
parent 65b8671b66
commit 8223d79886
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
49 changed files with 932 additions and 767 deletions

View file

@ -43,7 +43,7 @@
"tslib": "^2.6.2"
},
"devDependencies": {
"@types/node": "^20.12.7",
"@types/node": "^20.12.8",
"@yume-chan/eslint-config": "workspace:^1.0.0",
"@yume-chan/tsconfig": "workspace:^1.0.0",
"jest": "^30.0.0-alpha.3",

View file

@ -33,8 +33,8 @@ importers:
version: 2.6.2
devDependencies:
'@types/node':
specifier: ^20.12.7
version: 20.12.7
specifier: ^20.12.8
version: 20.12.8
'@yume-chan/eslint-config':
specifier: workspace:^1.0.0
version: link:../../toolchain/eslint-config
@ -43,7 +43,7 @@ importers:
version: link:../../toolchain/tsconfig
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -73,8 +73,8 @@ importers:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3
'@types/node':
specifier: ^20.12.7
version: 20.12.7
specifier: ^20.12.8
version: 20.12.8
'@yume-chan/eslint-config':
specifier: workspace:^1.0.0
version: link:../../toolchain/eslint-config
@ -86,7 +86,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -179,7 +179,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -203,8 +203,8 @@ importers:
version: link:../struct
devDependencies:
'@types/node':
specifier: ^20.12.7
version: 20.12.7
specifier: ^20.12.8
version: 20.12.8
'@yume-chan/eslint-config':
specifier: workspace:^1.0.0
version: link:../../toolchain/eslint-config
@ -213,7 +213,7 @@ importers:
version: link:../../toolchain/tsconfig
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -247,7 +247,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -294,7 +294,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -312,8 +312,8 @@ importers:
version: 4.0.3
devDependencies:
'@types/node':
specifier: ^20.12.7
version: 20.12.7
specifier: ^20.12.8
version: 20.12.8
../../libraries/no-data-view:
devDependencies:
@ -321,8 +321,8 @@ importers:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3
'@types/node':
specifier: ^20.12.7
version: 20.12.7
specifier: ^20.12.8
version: 20.12.8
'@yume-chan/eslint-config':
specifier: workspace:^1.0.0
version: link:../../toolchain/eslint-config
@ -334,7 +334,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -367,7 +367,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -410,7 +410,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -459,7 +459,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -502,7 +502,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -536,7 +536,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -567,7 +567,7 @@ importers:
version: 7.0.3
jest:
specifier: ^30.0.0-alpha.3
version: 30.0.0-alpha.3(@types/node@20.12.7)
version: 30.0.0-alpha.3(@types/node@20.12.8)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -581,20 +581,20 @@ importers:
../../toolchain/eslint-config:
dependencies:
'@eslint/js':
specifier: ^9.1.1
version: 9.1.1
specifier: ^9.2.0
version: 9.2.0
'@types/node':
specifier: ^20.12.7
version: 20.12.7
specifier: ^20.12.8
version: 20.12.8
eslint:
specifier: ^9.1.1
version: 9.1.1
specifier: ^9.2.0
version: 9.2.0
typescript:
specifier: ^5.4.5
version: 5.4.5
typescript-eslint:
specifier: ^7.8.0
version: 7.8.0(eslint@9.1.1)(typescript@5.4.5)
version: 7.8.0(eslint@9.2.0)(typescript@5.4.5)
devDependencies:
prettier:
specifier: ^3.2.5
@ -944,13 +944,13 @@ packages:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
/@eslint-community/eslint-utils@4.4.0(eslint@9.1.1):
/@eslint-community/eslint-utils@4.4.0(eslint@9.2.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
dependencies:
eslint: 9.1.1
eslint: 9.2.0
eslint-visitor-keys: 3.4.3
dev: false
@ -976,8 +976,8 @@ packages:
- supports-color
dev: false
/@eslint/js@9.1.1:
resolution: {integrity: sha512-5WoDz3Y19Bg2BnErkZTp0en+c/i9PvgFS7MBe1+m60HjFr0hrphlAGp4yzI7pxpt4xShln4ZyYp4neJm8hmOkQ==}
/@eslint/js@9.2.0:
resolution: {integrity: sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
dev: false
@ -1039,7 +1039,7 @@ packages:
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
dependencies:
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
jest-message-util: 30.0.0-alpha.3
jest-util: 30.0.0-alpha.3
@ -1060,14 +1060,14 @@ packages:
'@jest/test-result': 30.0.0-alpha.3
'@jest/transform': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 4.0.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 30.0.0-alpha.3
jest-config: 30.0.0-alpha.3(@types/node@20.12.7)
jest-config: 30.0.0-alpha.3(@types/node@20.12.8)
jest-haste-map: 30.0.0-alpha.3
jest-message-util: 30.0.0-alpha.3
jest-regex-util: 30.0.0-alpha.3
@ -1095,7 +1095,7 @@ packages:
dependencies:
'@jest/fake-timers': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
jest-mock: 30.0.0-alpha.3
dev: true
@ -1122,7 +1122,7 @@ packages:
dependencies:
'@jest/types': 30.0.0-alpha.3
'@sinonjs/fake-timers': 11.2.2
'@types/node': 20.12.7
'@types/node': 20.12.8
jest-message-util: 30.0.0-alpha.3
jest-mock: 30.0.0-alpha.3
jest-util: 30.0.0-alpha.3
@ -1155,7 +1155,7 @@ packages:
'@jest/transform': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@jridgewell/trace-mapping': 0.3.25
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@ -1250,7 +1250,7 @@ packages:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 20.12.7
'@types/node': 20.12.8
'@types/yargs': 17.0.32
chalk: 4.1.2
dev: true
@ -1262,7 +1262,7 @@ packages:
'@jest/schemas': 30.0.0-alpha.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 20.12.7
'@types/node': 20.12.8
'@types/yargs': 17.0.32
chalk: 4.1.2
dev: true
@ -1423,8 +1423,8 @@ packages:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: false
/@types/node@20.12.7:
resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==}
/@types/node@20.12.8:
resolution: {integrity: sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==}
dependencies:
undici-types: 5.26.5
@ -1450,7 +1450,7 @@ packages:
'@types/yargs-parser': 21.0.3
dev: true
/@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.1.1)(typescript@5.4.5):
/@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.2.0)(typescript@5.4.5):
resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
@ -1462,13 +1462,13 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.8.0(eslint@9.1.1)(typescript@5.4.5)
'@typescript-eslint/parser': 7.8.0(eslint@9.2.0)(typescript@5.4.5)
'@typescript-eslint/scope-manager': 7.8.0
'@typescript-eslint/type-utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5)
'@typescript-eslint/utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5)
'@typescript-eslint/type-utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5)
'@typescript-eslint/utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5)
'@typescript-eslint/visitor-keys': 7.8.0
debug: 4.3.4
eslint: 9.1.1
eslint: 9.2.0
graphemer: 1.4.0
ignore: 5.3.1
natural-compare: 1.4.0
@ -1479,7 +1479,7 @@ packages:
- supports-color
dev: false
/@typescript-eslint/parser@7.8.0(eslint@9.1.1)(typescript@5.4.5):
/@typescript-eslint/parser@7.8.0(eslint@9.2.0)(typescript@5.4.5):
resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
@ -1494,7 +1494,7 @@ packages:
'@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5)
'@typescript-eslint/visitor-keys': 7.8.0
debug: 4.3.4
eslint: 9.1.1
eslint: 9.2.0
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
@ -1508,7 +1508,7 @@ packages:
'@typescript-eslint/visitor-keys': 7.8.0
dev: false
/@typescript-eslint/type-utils@7.8.0(eslint@9.1.1)(typescript@5.4.5):
/@typescript-eslint/type-utils@7.8.0(eslint@9.2.0)(typescript@5.4.5):
resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
@ -1519,9 +1519,9 @@ packages:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5)
'@typescript-eslint/utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5)
'@typescript-eslint/utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5)
debug: 4.3.4
eslint: 9.1.1
eslint: 9.2.0
ts-api-utils: 1.3.0(typescript@5.4.5)
typescript: 5.4.5
transitivePeerDependencies:
@ -1555,19 +1555,19 @@ packages:
- supports-color
dev: false
/@typescript-eslint/utils@7.8.0(eslint@9.1.1)(typescript@5.4.5):
/@typescript-eslint/utils@7.8.0(eslint@9.2.0)(typescript@5.4.5):
resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
eslint: ^8.56.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.1.1)
'@eslint-community/eslint-utils': 4.4.0(eslint@9.2.0)
'@types/json-schema': 7.0.15
'@types/semver': 7.5.8
'@typescript-eslint/scope-manager': 7.8.0
'@typescript-eslint/types': 7.8.0
'@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5)
eslint: 9.1.1
eslint: 9.2.0
semver: 7.6.0
transitivePeerDependencies:
- supports-color
@ -2171,15 +2171,15 @@ packages:
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
dev: false
/eslint@9.1.1:
resolution: {integrity: sha512-b4cRQ0BeZcSEzPpY2PjFY70VbO32K7BStTGtBsnIGdTSEEQzBi8hPBcGQmTG2zUvFr9uLe0TK42bw8YszuHEqg==}
/eslint@9.2.0:
resolution: {integrity: sha512-0n/I88vZpCOzO+PQpt0lbsqmn9AsnsJAQseIqhZFI8ibQT0U1AkEKRxA3EVMos0BoHSXDQvCXY25TUjB5tr8Og==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.1.1)
'@eslint-community/eslint-utils': 4.4.0(eslint@9.2.0)
'@eslint-community/regexpp': 4.10.0
'@eslint/eslintrc': 3.0.2
'@eslint/js': 9.1.1
'@eslint/js': 9.2.0
'@humanwhocodes/config-array': 0.13.0
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.2.3
@ -2794,7 +2794,7 @@ packages:
'@jest/expect': 30.0.0-alpha.3
'@jest/test-result': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
co: 4.6.0
dedent: 1.5.3
@ -2815,7 +2815,7 @@ packages:
- supports-color
dev: true
/jest-cli@30.0.0-alpha.3(@types/node@20.12.7):
/jest-cli@30.0.0-alpha.3(@types/node@20.12.8):
resolution: {integrity: sha512-z1aQDxDe0VeDSEUeMr9MrfI5cc2SSCiKtG0Rt3XDfTgWrzyoakVds/9QMkkpNKHryCBzZZKOMe5W2uy7qM4WOA==}
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
hasBin: true
@ -2831,7 +2831,7 @@ packages:
chalk: 4.1.2
exit: 0.1.2
import-local: 3.1.0
jest-config: 30.0.0-alpha.3(@types/node@20.12.7)
jest-config: 30.0.0-alpha.3(@types/node@20.12.8)
jest-util: 30.0.0-alpha.3
jest-validate: 30.0.0-alpha.3
yargs: 17.7.2
@ -2842,7 +2842,7 @@ packages:
- ts-node
dev: true
/jest-config@30.0.0-alpha.3(@types/node@20.12.7):
/jest-config@30.0.0-alpha.3(@types/node@20.12.8):
resolution: {integrity: sha512-3eqS6gcsaPtcpU/VVlkLx1se1JiH18uh1Xg+oOf6FhlLDvAT5h6+dvWa2IpyucCN46dHHEw3E85qfjogq4XLtw==}
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
peerDependencies:
@ -2857,7 +2857,7 @@ packages:
'@babel/core': 7.24.5
'@jest/test-sequencer': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
babel-jest: 30.0.0-alpha.3(@babel/core@7.24.5)
chalk: 4.1.2
ci-info: 4.0.0
@ -2917,7 +2917,7 @@ packages:
'@jest/environment': 30.0.0-alpha.3
'@jest/fake-timers': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
jest-mock: 30.0.0-alpha.3
jest-util: 30.0.0-alpha.3
dev: true
@ -2932,7 +2932,7 @@ packages:
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
dependencies:
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@ -2983,7 +2983,7 @@ packages:
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
dependencies:
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
jest-util: 30.0.0-alpha.3
dev: true
@ -3038,7 +3038,7 @@ packages:
'@jest/test-result': 30.0.0-alpha.3
'@jest/transform': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@ -3069,7 +3069,7 @@ packages:
'@jest/test-result': 30.0.0-alpha.3
'@jest/transform': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
cjs-module-lexer: 1.3.1
collect-v8-coverage: 1.0.2
@ -3122,7 +3122,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -3134,7 +3134,7 @@ packages:
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
dependencies:
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
chalk: 4.1.2
ci-info: 4.0.0
graceful-fs: 4.2.11
@ -3159,7 +3159,7 @@ packages:
dependencies:
'@jest/test-result': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
'@types/node': 20.12.7
'@types/node': 20.12.8
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@ -3171,13 +3171,13 @@ packages:
resolution: {integrity: sha512-8lS9LxbEjOyBRz0Pdi6m3HYJ3feIi1tv0u7oqxjXvB1lMksq+IcSxaPTCcvJbIqt3WAFFYQnDs5I3NkJiEG5Ow==}
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
dependencies:
'@types/node': 20.12.7
'@types/node': 20.12.8
jest-util: 30.0.0-alpha.3
merge-stream: 2.0.0
supports-color: 8.1.1
dev: true
/jest@30.0.0-alpha.3(@types/node@20.12.7):
/jest@30.0.0-alpha.3(@types/node@20.12.8):
resolution: {integrity: sha512-oJndFRnG1Xsc1ybac44hGGj7+O4nT9losg8+8YDjNwDAXbYwvzyRgmCiPo6L/BROiAD8Z9qGgFRsFuGdpmQuFw==}
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
hasBin: true
@ -3190,7 +3190,7 @@ packages:
'@jest/core': 30.0.0-alpha.3
'@jest/types': 30.0.0-alpha.3
import-local: 3.1.0
jest-cli: 30.0.0-alpha.3(@types/node@20.12.7)
jest-cli: 30.0.0-alpha.3(@types/node@20.12.8)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -4017,7 +4017,7 @@ packages:
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
jest: 30.0.0-alpha.3(@types/node@20.12.7)
jest: 30.0.0-alpha.3(@types/node@20.12.8)
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
@ -4047,7 +4047,7 @@ packages:
engines: {node: '>=10'}
dev: true
/typescript-eslint@7.8.0(eslint@9.1.1)(typescript@5.4.5):
/typescript-eslint@7.8.0(eslint@9.2.0)(typescript@5.4.5):
resolution: {integrity: sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
@ -4057,10 +4057,10 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.1.1)(typescript@5.4.5)
'@typescript-eslint/parser': 7.8.0(eslint@9.1.1)(typescript@5.4.5)
'@typescript-eslint/utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5)
eslint: 9.1.1
'@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.2.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.8.0(eslint@9.2.0)(typescript@5.4.5)
'@typescript-eslint/utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5)
eslint: 9.2.0
typescript: 5.4.5
transitivePeerDependencies:
- supports-color

View file

@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "c1a7aa8a614d364cced2ff0b11ae2cd00e133961",
"pnpmShrinkwrapHash": "ca7743cb8480b533789651d98878a7900c9455fa",
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}

View file

@ -333,10 +333,10 @@ export class AdbScrcpyClient {
async #parseDeviceMessages(controlStream: ReadableStream<Uint8Array>) {
const buffered = new BufferedReadableStream(controlStream);
while (true) {
const type = await buffered.readExactly(1);
if (!(await this.#options.parseDeviceMessage(type[0]!, buffered))) {
const [type] = await buffered.readExactly(1);
if (!(await this.#options.parseDeviceMessage(type!, buffered))) {
buffered
.cancel(new Error(`Unknown device message type ${type[0]}`))
.cancel(new Error(`Unknown device message type ${type!}`))
.catch(() => {});
break;
}

View file

@ -94,6 +94,15 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
const buffered = new BufferedReadableStream(
stream.readable,
);
// Skip the dummy byte
// Google ADB forward tunnel listens on a socket on the computer,
// when a client connects to that socket, Google ADB will forward
// the connection to the socket on the device.
// However, connecting to that socket will always succeed immediately,
// which doesn't mean that Google ADB has connected to
// the socket on the device.
// Thus Scrcpy server sends a dummy byte to the socket, to let the client
// know that the connection is truly established.
await buffered.readExactly(1);
return {
readable: buffered.release(),

View file

@ -37,7 +37,7 @@
"@yume-chan/struct": "workspace:^0.0.23"
},
"devDependencies": {
"@types/node": "^20.12.7",
"@types/node": "^20.12.8",
"@yume-chan/eslint-config": "workspace:^1.0.0",
"@yume-chan/tsconfig": "workspace:^1.0.0",
"jest": "^30.0.0-alpha.3",

View file

@ -40,7 +40,7 @@
},
"devDependencies": {
"@jest/globals": "^30.0.0-alpha.3",
"@types/node": "^20.12.7",
"@types/node": "^20.12.8",
"@yume-chan/eslint-config": "workspace:^1.0.0",
"@yume-chan/tsconfig": "workspace:^1.0.0",
"cross-env": "^7.0.3",

View file

@ -2,10 +2,10 @@
import { AutoDisposable } from "@yume-chan/event";
import { BufferedReadableStream } from "@yume-chan/stream-extra";
import Struct, { ExactReadableEndedError } from "@yume-chan/struct";
import Struct, { ExactReadableEndedError, encodeUtf8 } from "@yume-chan/struct";
import type { Adb, AdbIncomingSocketHandler } from "../adb.js";
import { decodeUtf8, hexToNumber } from "../utils/index.js";
import { hexToNumber } from "../utils/index.js";
export interface AdbForwardListener {
deviceSerial: string;
@ -47,10 +47,20 @@ const AdbReverseErrorResponse = new Struct()
}
});
async function readString(stream: BufferedReadableStream, length: number) {
const buffer = await stream.readExactly(length);
return decodeUtf8(buffer);
// Like `hexToNumber`, it's much faster than first converting `buffer` to a string
function decimalToNumber(buffer: Uint8Array) {
let value = 0;
for (const byte of buffer) {
// Like `parseInt`, return when it encounters a non-digit character
if (byte < 48 || byte > 57) {
return value;
}
value = value * 10 + byte - 48;
}
return value;
}
const OKAY = encodeUtf8("OKAY");
export class AdbReverseCommand extends AutoDisposable {
protected adb: Adb;
@ -70,10 +80,14 @@ export class AdbReverseCommand extends AutoDisposable {
protected async sendRequest(service: string) {
const stream = await this.createBufferedStream(service);
const success = (await readString(stream, 4)) === "OKAY";
if (!success) {
const response = await stream.readExactly(4);
for (let i = 0; i < 4; i += 1) {
if (response[i] !== OKAY[i]) {
await AdbReverseErrorResponse.deserialize(stream);
}
}
return stream;
}
@ -110,8 +124,8 @@ export class AdbReverseCommand extends AutoDisposable {
const position = stream.position;
try {
const length = hexToNumber(await stream.readExactly(4));
const port = await readString(stream, length);
deviceAddress = `tcp:${Number.parseInt(port, 10)}`;
const port = decimalToNumber(await stream.readExactly(length));
deviceAddress = `tcp:${port}`;
} catch (e) {
if (
e instanceof ExactReadableEndedError &&

View file

@ -139,14 +139,12 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
this.#stdin = new MaybeConsumable.WritableStream<Uint8Array>({
write: async (chunk) => {
await MaybeConsumable.tryConsume(chunk, async (chunk) => {
await this.#writer.write(
AdbShellProtocolPacket.serialize({
id: AdbShellProtocolId.Stdin,
data: chunk,
}),
);
});
},
});
}

View file

@ -35,7 +35,8 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
this.#combiner = new BufferCombiner(bufferSize);
}
async #writeConsumable(buffer: Uint8Array) {
async #write(buffer: Uint8Array) {
// `#combiner` will reuse the buffer, so we need to use the Consumable pattern
await Consumable.WritableStream.write(this.#writer, buffer);
}
@ -44,7 +45,7 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
await this.#writeLock.wait();
const buffer = this.#combiner.flush();
if (buffer) {
await this.#writeConsumable(buffer);
await this.#write(buffer);
}
} finally {
this.#writeLock.notifyOne();
@ -55,7 +56,7 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
try {
await this.#writeLock.wait();
for (const buffer of this.#combiner.push(data)) {
await this.#writeConsumable(buffer);
await this.#write(buffer);
}
} finally {
this.#writeLock.notifyOne();
@ -63,11 +64,15 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
}
async readExactly(length: number) {
// The request may still be in the internal buffer.
// Call `flush` to send it before starting reading
await this.flush();
return await this.#readable.readExactly(length);
}
release(): void {
// In theory, the writer shouldn't leave anything in the buffer,
// but to be safe, call `flush` to throw away any remaining data.
this.#combiner.flush();
this.#socketLock.notifyOne();
}

View file

@ -24,27 +24,56 @@ import { AdbCommand, calculateChecksum } from "./packet.js";
import { AdbDaemonSocketController } from "./socket.js";
export interface AdbPacketDispatcherOptions {
calculateChecksum: boolean;
/**
* Before Android 9.0, ADB uses `char*` to parse service string,
* From Android 9.0, ADB stopped checking the checksum in packet header to improve performance.
*
* The value should be inferred from the device's ADB protocol version.
*/
calculateChecksum: boolean;
/**
* Before Android 9.0, ADB uses `char*` to parse service strings,
* thus requires a null character to terminate.
*
* Usually it should have the same value as `calculateChecksum`.
* The value should be inferred from the device's ADB protocol version.
* Usually it should have the same value as `calculateChecksum`, since they both changed
* in Android 9.0.
*/
appendNullToServiceString: boolean;
maxPayloadSize: number;
/**
* The number of bytes the device can send before receiving an ack packet.
* Set to 0 or any negative value to disable delayed ack.
* Otherwise the value must be in the range of unsigned 32-bit integer.
*/
initialDelayedAckBytes: number;
maxPayloadSize: number;
/**
* Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
* Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`)
* when `AdbPacketDispatcher.close` is called.
*
* @default false
*/
preserveConnection?: boolean | undefined;
debugSlowRead?: boolean | undefined;
/**
* The number of bytes the device can send before receiving an ack packet.
* Using delayed ack can improve the throughput,
* especially when the device is connected over Wi-Fi (so the latency is higher).
*
* This must be the negotiated value between the client and device. If the device enabled
* delayed ack but the client didn't, the device will throw an error when the client sends
* the first `WRTE` packet. And vice versa.
*/
initialDelayedAckBytes: number;
/**
* When set, the dispatcher will throw an error when
* one of the socket readable stalls for this amount of milliseconds.
*
* Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets.
* It's important to always read from all sockets to prevent stalling.
*
* This option is helpful to detect bugs in the client code.
*
* @default false
*/
readTimeLimit?: number | undefined;
}
interface SocketOpenResult {
@ -225,12 +254,19 @@ export class AdbPacketDispatcher implements Closeable {
// Maybe the device is responding to a packet of last connection
// Tell the device to close the socket
void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
void this.sendPacket(
AdbCommand.Close,
packet.arg1,
packet.arg0,
EMPTY_UINT8_ARRAY,
);
}
#sendOkay(localId: number, remoteId: number, ackBytes: number) {
let payload: Uint8Array;
if (this.options.initialDelayedAckBytes !== 0) {
// TODO: try reusing this buffer to reduce memory allocation
// However, that requires blocking reentrance of `sendOkay`, which might be more expensive
payload = new Uint8Array(4);
setUint32LittleEndian(payload, 0, ackBytes);
} else {
@ -241,22 +277,24 @@ export class AdbPacketDispatcher implements Closeable {
}
async #handleOpen(packet: AdbPacketData) {
// `AsyncOperationManager` doesn't support skipping IDs
// Use `add` + `resolve` to simulate this behavior
// Allocate a local ID for the socket from `#initializers`.
// `AsyncOperationManager` doesn't directly support returning the next ID,
// so use `add` + `resolve` to simulate this
const [localId] = this.#initializers.add<number>();
this.#initializers.resolve(localId, undefined);
const remoteId = packet.arg0;
let initialDelayedAckBytes = packet.arg1;
let availableWriteBytes = packet.arg1;
const service = decodeUtf8(packet.payload);
// Check remote delayed ack enablement is consistent with local
if (this.options.initialDelayedAckBytes === 0) {
if (initialDelayedAckBytes !== 0) {
if (availableWriteBytes !== 0) {
throw new Error("Invalid OPEN packet. arg1 should be 0");
}
initialDelayedAckBytes = Infinity;
availableWriteBytes = Infinity;
} else {
if (initialDelayedAckBytes === 0) {
if (availableWriteBytes === 0) {
throw new Error(
"Invalid OPEN packet. arg1 should be greater than 0",
);
@ -265,7 +303,12 @@ export class AdbPacketDispatcher implements Closeable {
const handler = this.#incomingSocketHandlers.get(service);
if (!handler) {
await this.sendPacket(AdbCommand.Close, 0, remoteId);
await this.sendPacket(
AdbCommand.Close,
0,
remoteId,
EMPTY_UINT8_ARRAY,
);
return;
}
@ -275,8 +318,8 @@ export class AdbPacketDispatcher implements Closeable {
remoteId,
localCreated: false,
service,
availableWriteBytes,
});
controller.ack(initialDelayedAckBytes);
try {
await handler(controller.socket);
@ -287,7 +330,12 @@ export class AdbPacketDispatcher implements Closeable {
this.options.initialDelayedAckBytes,
);
} catch (e) {
await this.sendPacket(AdbCommand.Close, 0, remoteId);
await this.sendPacket(
AdbCommand.Close,
0,
remoteId,
EMPTY_UINT8_ARRAY,
);
}
}
@ -298,14 +346,8 @@ export class AdbPacketDispatcher implements Closeable {
}
let handled = false;
await Promise.race([
delay(5000).then(() => {
if (this.options.debugSlowRead && !handled) {
throw new Error(
`packet for \`${socket.service}\` not handled in 5 seconds`,
);
}
}),
const promises: Promise<void>[] = [
(async () => {
await socket.enqueue(packet.payload);
await this.#sendOkay(
@ -315,9 +357,22 @@ export class AdbPacketDispatcher implements Closeable {
);
handled = true;
})(),
]);
];
return;
if (this.options.readTimeLimit) {
promises.push(
(async () => {
await delay(this.options.readTimeLimit!);
if (!handled) {
throw new Error(
`readable of \`${socket.service}\` has stalled for ${this.options.readTimeLimit} milliseconds`,
);
}
})(),
);
}
await Promise.race(promises);
}
async createSocket(service: string): Promise<AdbSocket> {
@ -342,8 +397,8 @@ export class AdbPacketDispatcher implements Closeable {
remoteId,
localCreated: true,
service,
availableWriteBytes,
});
controller.ack(availableWriteBytes);
this.#sockets.set(localId, controller);
return controller.socket;
@ -365,7 +420,8 @@ export class AdbPacketDispatcher implements Closeable {
command: AdbCommand,
arg0: number,
arg1: number,
payload: string | Uint8Array = EMPTY_UINT8_ARRAY,
// PERF: It's slightly faster to not use default parameter values
payload: string | Uint8Array,
): Promise<void> {
if (typeof payload === "string") {
payload = encodeUtf8(payload);

View file

@ -1,8 +1,4 @@
import type { Consumable } from "@yume-chan/stream-extra";
import {
ConsumableReadableStream,
TransformStream,
} from "@yume-chan/stream-extra";
import { Consumable, TransformStream } from "@yume-chan/stream-extra";
import Struct from "@yume-chan/struct";
export enum AdbCommand {
@ -65,7 +61,7 @@ export class AdbPacketSerializeStream extends TransformStream<
const init = chunk as AdbPacketInit & AdbPacketHeaderInit;
init.payloadLength = init.payload.byteLength;
await ConsumableReadableStream.enqueue(
await Consumable.ReadableStream.enqueue(
controller,
AdbPacketHeader.serialize(init, headerBuffer),
);
@ -74,7 +70,7 @@ export class AdbPacketSerializeStream extends TransformStream<
// USB protocol preserves packet boundaries,
// so we must write payload separately as native ADB does,
// otherwise the read operation on device will fail.
await ConsumableReadableStream.enqueue(
await Consumable.ReadableStream.enqueue(
controller,
init.payload,
);

View file

@ -9,6 +9,7 @@ import {
type WritableStream,
type WritableStreamDefaultController,
} from "@yume-chan/stream-extra";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import type { AdbSocket } from "../adb.js";
@ -23,11 +24,15 @@ export interface AdbDaemonSocketInfo {
service: string;
}
export interface AdbDaemonSocketConstructionOptions
extends AdbDaemonSocketInfo {
export interface AdbDaemonSocketInit extends AdbDaemonSocketInfo {
dispatcher: AdbPacketDispatcher;
highWaterMark?: number | undefined;
/**
* The initial delayed ack byte count, or `Infinity` if delayed ack is disabled.
*/
availableWriteBytes: number;
}
export class AdbDaemonSocketController
@ -72,7 +77,7 @@ export class AdbDaemonSocketController
*/
#availableWriteBytes = 0;
constructor(options: AdbDaemonSocketConstructionOptions) {
constructor(options: AdbDaemonSocketInit) {
this.#dispatcher = options.dispatcher;
this.localId = options.localId;
this.remoteId = options.remoteId;
@ -102,6 +107,7 @@ export class AdbDaemonSocketController
});
this.#socket = new AdbDaemonSocket(this);
this.#availableWriteBytes = options.availableWriteBytes;
}
async #writeChunk(data: Uint8Array, signal: AbortSignal) {
@ -172,6 +178,7 @@ export class AdbDaemonSocketController
AdbCommand.Close,
this.localId,
this.remoteId,
EMPTY_UINT8_ARRAY,
);
}

View file

@ -56,26 +56,52 @@ export type AdbDaemonConnection = ReadableWritablePair<
Consumable<AdbPacketInit>
>;
interface AdbDaemonAuthenticationOptions {
export interface AdbDaemonAuthenticationOptions {
serial: string;
connection: AdbDaemonConnection;
credentialStore: AdbCredentialStore;
authenticators?: AdbAuthenticator[];
features?: readonly AdbFeature[];
/**
* The number of bytes the device can send before receiving an ack packet.
* Using delayed ack can improve the throughput,
* especially when the device is connected over Wi-Fi (so the latency is higher).
*
* Set to 0 or any negative value to disable delayed ack in handshake.
* Otherwise the value must be in the range of unsigned 32-bit integer.
*
* Delayed ack requires Android 14, this option is ignored on older versions.
* Delayed ack was added in Android 14,
* this option will be ignored when the device doesn't support it.
*
* @default ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE
*/
initialDelayedAckBytes?: number;
/**
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
* Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`)
* when `AdbDaemonTransport.close` is called.
*
* Note that when `authenticate` fails,
* no matter which value this option has,
* the `connection` is always kept open, so it can be used in another `authenticate` call.
*
* @default false
*/
preserveConnection?: boolean | undefined;
debugSlowRead?: boolean | undefined;
/**
* When set, the transport will throw an error when
* one of the socket readable stalls for this amount of milliseconds.
*
* Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets.
* It's important to always read from all sockets to prevent stalling.
*
* This option is helpful to detect bugs in the client code.
*
* @default undefined
*/
readTimeLimit?: number | undefined;
}
interface AdbDaemonSocketConnectorConstructionOptions {
@ -85,20 +111,39 @@ interface AdbDaemonSocketConnectorConstructionOptions {
maxPayloadSize: number;
banner: string;
features?: readonly AdbFeature[];
/**
* The number of bytes the device can send before receiving an ack packet.
* Using delayed ack can improve the throughput,
* especially when the device is connected over Wi-Fi (so the latency is higher).
*
* Set to 0 or any negative value to disable delayed ack in handshake.
* Otherwise the value must be in the range of unsigned 32-bit integer.
*
* Delayed ack requires Android 14, this option is ignored on older versions.
* When `features` doesn't include `AdbFeature.DelayedAck`, it must be set to 0. Otherwise,
* the value must be in the range of unsigned 32-bit integer. If the device enabled
* delayed ack but the client didn't, the device will throw an error when the client sends
* the first data packet. And vice versa.
*/
initialDelayedAckBytes?: number;
initialDelayedAckBytes: number;
/**
* Whether to preserve the connection open after the `AdbDaemonTransport` is closed.
* Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`)
* when `AdbDaemonTransport.close` is called.
*
* @default false
*/
preserveConnection?: boolean | undefined;
debugSlowRead?: boolean | undefined;
/**
* When set, the transport will throw an error when
* one of the socket readable stalls for this amount of milliseconds.
*
* Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets.
* It's important to always read from all sockets to prevent stalling.
*
* This option is helpful to detect bugs in the client code.
*
* @default undefined
*/
readTimeLimit?: number | undefined;
}
export class AdbDaemonTransport implements AdbTransport {
@ -106,9 +151,9 @@ export class AdbDaemonTransport implements AdbTransport {
* Authenticates the connection and creates an `AdbDaemonTransport` instance
* that can be used by `Adb` class.
*
* If an authentication process failed, it's possible to call `authenticate` again
* on the same connection. Because every time the device receives a `CNXN` packet,
* it resets all internal state, and starts a new authentication process.
* If an authentication process failed,
* no matter which value the `preserveConnection` option has,
* the `connection` is always kept open, so it can be used in another `authenticate` call.
*/
static async authenticate({
serial,
@ -278,7 +323,7 @@ export class AdbDaemonTransport implements AdbTransport {
version,
banner,
features = ADB_DAEMON_DEFAULT_FEATURES,
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
initialDelayedAckBytes,
...options
}: AdbDaemonSocketConnectorConstructionOptions) {
this.#serial = serial;

View file

@ -27,7 +27,12 @@ import {
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
import { AdbBanner } from "../banner.js";
import type { AdbFeature } from "../features.js";
import { NOOP, hexToNumber, numberToHex, unreachable } from "../utils/index.js";
import {
NOOP,
hexToNumber,
unreachable,
write4HexDigits,
} from "../utils/index.js";
import { AdbServerTransport } from "./transport.js";
@ -76,6 +81,23 @@ export interface AdbServerDevice {
transportId: bigint;
}
function sequenceEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
const OKAY = encodeUtf8("OKAY");
const FAIL = encodeUtf8("FAIL");
export class AdbServerClient {
static readonly VERSION = 41;
@ -99,8 +121,27 @@ export class AdbServerClient {
return stream.readExactly(length);
}
})
.then((valueBuffer) => {
return decodeUtf8(valueBuffer);
.then((buffer) => {
// TODO: Investigate using stream mode `TextDecoder` for long strings.
// Because concatenating strings uses rope data structure,
// which only points to the original strings and doesn't copy the data,
// it's more efficient than concatenating `Uint8Array`s.
//
// ```
// const decoder = new TextDecoder();
// let result = '';
// for await (const chunk of stream.iterateExactly(length)) {
// result += decoder.decode(chunk, { stream: true });
// }
// result += decoder.decode();
// return result;
// ```
//
// Although, it will be super complex to use `SyncPromise` with async iterator,
// `stream.iterateExactly` need to return an
// `Iterator<Uint8Array | Promise<Uint8Array>>` instead of a true async iterator.
// Maybe `SyncPromise` should support async iterators directly.
return decodeUtf8(buffer);
})
.valueOrPromise();
}
@ -109,27 +150,29 @@ export class AdbServerClient {
writer: WritableStreamDefaultWriter<Uint8Array>,
value: string,
): Promise<void> {
const valueBuffer = encodeUtf8(value);
const buffer = new Uint8Array(4 + valueBuffer.length);
buffer.set(numberToHex(valueBuffer.length));
buffer.set(valueBuffer, 4);
// TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
// That way allocates a new string (hopefully only a rope) instead of a new buffer
const encoded = encodeUtf8(value);
const buffer = new Uint8Array(4 + encoded.length);
write4HexDigits(buffer, 0, encoded.length);
buffer.set(encoded, 4);
await writer.write(buffer);
}
static async readOkay(
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
const response = decodeUtf8(await stream.readExactly(4));
if (response === "OKAY") {
const response = await stream.readExactly(4);
if (sequenceEqual(response, OKAY)) {
return;
}
if (response === "FAIL") {
if (sequenceEqual(response, FAIL)) {
const reason = await AdbServerClient.readString(stream);
throw new Error(reason);
}
throw new Error(`Unexpected response: ${response}`);
throw new Error(`Unexpected response: ${decodeUtf8(response)}`);
}
async connect(

View file

@ -79,40 +79,51 @@ export function encodeBase64(
output.byteOffset + output.length - (paddingLength + 1) <=
input.byteOffset + input.length
) {
// Output ends before input ends
// So output won't catch up with input.
// Output ends before input ends.
// Can encode forwards, because writing output won't catch up with reading input.
// Depends on padding length,
// it's possible to write 1-3 bytes after input ends.
// spell: disable-next-line
// | aaaaaabb | | | |
// | aaaaaa | bb0000 | = | = |
// The output end is subtracted by `(paddingLength + 1)` because
// depending on padding length, it's possible to write 1-3 extra bytes after input ends.
//
// spell: disable-next-line
// | aaaaaabb | bbbbcccc | | |
// | aaaaaa | bbbbbb | cccc00 | = |
// The following diagrams show how the last read from input and the last write to output
// are not conflicting.
//
// spell: disable-next-line
// | aaaaaabb | bbbbcccc | ccdddddd | |
// | aaaaaa | bbbbbb | cccccc | dddddd |
// spell: disable
//
// `paddingLength === 2` can write 3 extra bytes:
//
// input: | aaaaaabb | | | |
// output: | aaaaaa | bb0000 | = | = |
//
// `paddingLength === 1` can write 2 extra bytes:
//
// input: | aaaaaabb | bbbbcccc | | |
// output: | aaaaaa | bbbbbb | cccc00 | = |
//
// `paddingLength === 0` can write 1 extra byte:
//
// input: | aaaaaabb | bbbbcccc | ccdddddd | |
// output: | aaaaaa | bbbbbb | cccccc | dddddd |
//
// spell: enable
// Must encode forwards.
encodeForward(input, output, paddingLength);
} else if (output.byteOffset >= input.byteOffset - 1) {
// Output starts after input starts
// So in backwards, output can't catch up with input.
// So in backwards, writing output won't catch up with reading input.
// Because first 3 bytes becomes 4 bytes,
// it's possible to write 1 byte before input starts.
// The input start is subtracted by `1`, Because as the first 3 bytes becomes 4 bytes,
// it's possible to write 1 extra byte before input starts.
// spell: disable-next-line
// | | aaaaaabb | bbbbcccc | ccdddddd |
// | aaaaaa | bbbbbb | cccccc | dddddd |
// input: | | aaaaaabb | bbbbcccc | ccdddddd |
// output: | aaaaaa | bbbbbb | cccccc | dddddd |
// Must encode backwards.
encodeBackward(input, output, paddingLength);
} else {
// Input is in the middle of output,
// not possible to read neither first or last three bytes,
// It's not possible to read either the first or the last three bytes
// before they are overwritten by the output.
throw new Error("input and output cannot overlap");
}

View file

@ -36,23 +36,25 @@ export function hexToNumber(data: Uint8Array): number {
return result;
}
export function numberToHex(value: number) {
const result = new Uint8Array(4);
let index = 3;
while (index >= 0 && value > 0) {
export function write4HexDigits(
buffer: Uint8Array,
index: number,
value: number,
) {
const start = index;
index += 3;
while (index >= start && value > 0) {
const digit = value & 0xf;
value >>= 4;
if (digit < 10) {
result[index] = digit + 48;
buffer[index] = digit + 48; // '0'
} else {
result[index] = digit + 87;
buffer[index] = digit + 87; // 'a' - 10
}
index -= 1;
}
while (index >= 0) {
// '0'
result[index] = 48;
while (index >= start) {
buffer[index] = 48; // '0'
index -= 1;
}
return result;
}

View file

@ -353,6 +353,7 @@ export async function deserializeAndroidLogEntry(
): Promise<AndroidLogEntry> {
const entry = (await LoggerEntry.deserialize(stream)) as AndroidLogEntry;
if (entry.headerSize !== LoggerEntry.size) {
// Skip unknown fields
await stream.readExactly(entry.headerSize - LoggerEntry.size);
}

View file

@ -30,6 +30,6 @@
"gh-release-fetch": "^4.0.3"
},
"devDependencies": {
"@types/node": "^20.12.7"
"@types/node": "^20.12.8"
}
}

View file

@ -31,7 +31,7 @@
},
"devDependencies": {
"@jest/globals": "^30.0.0-alpha.3",
"@types/node": "^20.12.7",
"@types/node": "^20.12.8",
"@yume-chan/eslint-config": "workspace:^1.0.0",
"@yume-chan/tsconfig": "workspace:^1.0.0",
"cross-env": "^7.0.3",

View file

@ -344,8 +344,11 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
return;
}
// WebCodecs requires configuration data to be with the first frame.
// For H.264 and H.265, when the stream is in Annex B format
// (which Scrcpy uses, as Android MediaCodec produces),
// configuration data needs to be combined with the first frame data.
// https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type
// AV1 doesn't need to do this, the handling code also doesn't set `#config`.
let data: Uint8Array;
if (this.#config !== undefined) {
data = new Uint8Array(

View file

@ -91,145 +91,6 @@ export function* annexBSplitNalu(buffer: Uint8Array): Generator<Uint8Array> {
yield buffer.subarray(start, buffer.length);
}
/**
* Remove emulation prevention bytes from a H.264/H.265 NAL Unit.
*
* The input is not modified.
* If the input doesn't contain any emulation prevention bytes,
* the input is returned as-is.
* Otherwise, a new `Uint8Array` is created and returned.
*/
export function naluRemoveEmulation(buffer: Uint8Array) {
// output will be created when first emulation prevention byte is found
let output: Uint8Array | undefined;
let outputOffset = 0;
let zeroCount = 0;
let inEmulation = false;
let i = 0;
scan: for (; i < buffer.length; i += 1) {
const byte = buffer[i]!;
if (byte === 0x00) {
zeroCount += 1;
continue;
}
// Current byte is not zero
const prevZeroCount = zeroCount;
zeroCount = 0;
if (prevZeroCount < 2) {
// zero or one `0x00`s are acceptable
continue;
}
if (byte === 0x01) {
// Unexpected start code
throw new Error("Invalid data");
}
if (prevZeroCount > 2) {
// Too much `0x00`s
throw new Error("Invalid data");
}
switch (byte) {
case 0x02:
// Didn't find why, but 7.4.1 NAL unit semantics forbids `0x000002` appearing in NAL units
throw new Error("Invalid data");
case 0x03:
// `0x000003` is the "emulation_prevention_three_byte"
// `0x00000300`, `0x00000301`, `0x00000302` and `0x00000303` represent
// `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively
inEmulation = true;
// Create output and copy the data before the emulation prevention byte
// Output size is unknown, so we use the input size as an upper bound
output = new Uint8Array(buffer.length - 1);
output.set(buffer.subarray(0, i));
outputOffset = i;
i += 1;
break scan;
default:
// `0x000004` or larger are as-is
break;
}
}
if (!output) {
return buffer;
}
// Continue at the byte after the emulation prevention byte
for (; i < buffer.length; i += 1) {
const byte = buffer[i]!;
output[outputOffset] = byte;
outputOffset += 1;
if (inEmulation) {
if (byte > 0x03) {
// `0x00000304` or larger are invalid
throw new Error("Invalid data");
}
// `00000300000300` results in `0000000000` (both `0x03` are removed)
// which means the `0x00` after `0x03` also counts
if (byte === 0x00) {
zeroCount += 1;
}
inEmulation = false;
continue;
}
if (byte === 0x00) {
zeroCount += 1;
continue;
}
const prevZeroCount = zeroCount;
zeroCount = 0;
if (prevZeroCount < 2) {
// zero or one `0x00`s are acceptable
continue;
}
if (byte === 0x01) {
// Unexpected start code
throw new Error("Invalid data");
}
if (prevZeroCount > 2) {
// Too much `0x00`s
throw new Error("Invalid data");
}
switch (byte) {
case 0x02:
// Didn't find why, but 7.4.1 NAL unit semantics forbids `0x000002` appearing in NAL units
throw new Error("Invalid data");
case 0x03:
// `0x000003` is the "emulation_prevention_three_byte"
// `0x00000300`, `0x00000301`, `0x00000302` and `0x00000303` represent
// `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively
inEmulation = true;
// Remove the emulation prevention byte
outputOffset -= 1;
break;
default:
// `0x000004` or larger are as-is
break;
}
}
return output.subarray(0, outputOffset);
}
export class NaluSodbBitReader {
readonly #nalu: Uint8Array;
// logical length is `#byteLength * 8 + (7 - #stopBitIndex)`
@ -269,6 +130,7 @@ export class NaluSodbBitReader {
constructor(nalu: Uint8Array) {
this.#nalu = nalu;
// Search for the last bit being `1`, also known as the stop bit
for (let i = nalu.length - 1; i >= 0; i -= 1) {
if (this.#nalu[i] === 0) {
continue;
@ -292,16 +154,18 @@ export class NaluSodbBitReader {
this.#byte = this.#nalu[this.#bytePosition]!;
// If the current sequence is `0x000003`, skip to the next byte.
// `annexBSplitNalu` had validated the input, so don't need to check here.
// `annexBSplitNalu` had validated the input, so skip the check here
if (this.#zeroCount === 2 && this.#byte === 3) {
this.#zeroCount = 0;
this.#bytePosition += 1;
// Call `#loadByte` again, because if the next byte is `0x00`,
// it need to be counted in `#zeroCount` as well.
this.#loadByte();
return;
}
// `0x00000301` becomes `0x000001`, so only the `0x03` byte needs to be skipped
// The `0x00` bytes are still returned as-is
// All `0x00` bytes are returned as-is
if (this.#byte === 0) {
this.#zeroCount += 1;
} else {
@ -338,7 +202,13 @@ export class NaluSodbBitReader {
return result;
}
#ensurePositionValid() {
#checkSkipPosition() {
// This is different from `ended`,
// as it allows the bit position to be at the stop bit.
// In this case, there is no more bits to read, `ended` is `true`,
// and the next `next` call will throw an error.
// However, it's still a valid position for `skip`, which can skip all remaining bits,
// and stop at the end position.
if (
this.#bytePosition >= this.#byteLength &&
this.#bitPosition < this.#stopBitIndex
@ -350,24 +220,29 @@ export class NaluSodbBitReader {
skip(length: number) {
if (length <= this.#bitPosition + 1) {
this.#bitPosition -= length;
this.#ensurePositionValid();
this.#checkSkipPosition();
return;
}
// Because of emulation prevention bytes,
// we don't know how many bits are left in the NAL,
// nor how many bits should be skipped.
// So we need to check each byte.
length -= this.#bitPosition + 1;
this.#bytePosition += 1;
this.#bitPosition = 7;
this.#loadByte();
this.#ensurePositionValid();
this.#checkSkipPosition();
for (; length >= 8; length -= 8) {
this.#bytePosition += 1;
this.#loadByte();
this.#ensurePositionValid();
this.#checkSkipPosition();
}
this.#bitPosition = 7 - length;
this.#ensurePositionValid();
this.#checkSkipPosition();
}
decodeExponentialGolombNumber(): number {

View file

@ -1,5 +1,5 @@
import { getUint16 } from "@yume-chan/no-data-view";
import type { NumberFieldType } from "@yume-chan/struct";
import type { NumberFieldVariant } from "@yume-chan/struct";
import { NumberFieldDefinition } from "@yume-chan/struct";
export function clamp(value: number, min: number, max: number): number {
@ -14,7 +14,7 @@ export function clamp(value: number, min: number, max: number): number {
return value;
}
export const ScrcpyFloatToUint16NumberType: NumberFieldType = {
export const ScrcpyUnsignedFloatNumberVariant: NumberFieldVariant = {
size: 2,
signed: false,
deserialize(array, littleEndian) {
@ -30,6 +30,6 @@ export const ScrcpyFloatToUint16NumberType: NumberFieldType = {
},
};
export const ScrcpyFloatToUint16FieldDefinition = new NumberFieldDefinition(
ScrcpyFloatToUint16NumberType,
export const ScrcpyUnsignedFloatFieldDefinition = new NumberFieldDefinition(
ScrcpyUnsignedFloatNumberVariant,
);

View file

@ -1,5 +1,5 @@
export * from "./codec-options.js";
export * from "./float-to-uint16.js";
export * from "./float.js";
export * from "./init.js";
export * from "./message.js";
export * from "./options.js";

View file

@ -6,7 +6,7 @@ import {
ScrcpyControlMessageType,
} from "../../control/index.js";
import { ScrcpyFloatToUint16FieldDefinition } from "./float-to-uint16.js";
import { ScrcpyUnsignedFloatFieldDefinition } from "./float.js";
export const SCRCPY_CONTROL_MESSAGE_TYPES_1_16: readonly ScrcpyControlMessageType[] =
[
@ -38,7 +38,7 @@ export const ScrcpyInjectTouchControlMessage1_16 = new Struct()
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("pressure", ScrcpyFloatToUint16FieldDefinition)
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
.uint32("buttons");
export type ScrcpyInjectTouchControlMessage1_16 =

View file

@ -83,13 +83,20 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
return order.map((key) => toScrcpyOptionValue(options[key], "-"));
}
/**
* Parse a fixed-length, null-terminated string.
* @param stream The stream to read from
* @param maxLength The maximum length of the string, including the null terminator, in bytes
* @returns The parsed string, without the null terminator
*/
static async parseCString(
stream: AsyncExactReadable,
maxLength: number,
): Promise<string> {
let result = decodeUtf8(await stream.readExactly(maxLength));
result = result.substring(0, result.indexOf("\0"));
return result;
const buffer = await stream.readExactly(maxLength);
// If null terminator is not found, `subarray(0, -1)` will remove the last byte
// But since it's a invalid case, it's fine
return decodeUtf8(buffer.subarray(0, buffer.indexOf(0)));
}
static async parseUint16BE(stream: AsyncExactReadable): Promise<number> {

View file

@ -3,29 +3,29 @@ import { describe, expect, it } from "@jest/globals";
import { ScrcpyControlMessageType } from "../../control/index.js";
import {
ScrcpyFloatToInt16NumberType,
ScrcpyScrollController1_25,
ScrcpySignedFloatNumberVariant,
} from "./scroll.js";
describe("ScrcpyFloatToInt16NumberType", () => {
it("should serialize", () => {
const dataView = new DataView(new ArrayBuffer(2));
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, -1, true);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, -1, true);
expect(dataView.getInt16(0, true)).toBe(-0x8000);
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 0, true);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 0, true);
expect(dataView.getInt16(0, true)).toBe(0);
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 1, true);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 1, true);
expect(dataView.getInt16(0, true)).toBe(0x7fff);
});
it("should clamp input values", () => {
const dataView = new DataView(new ArrayBuffer(2));
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, -2, true);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, -2, true);
expect(dataView.getInt16(0, true)).toBe(-0x8000);
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 2, true);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 2, true);
expect(dataView.getInt16(0, true)).toBe(0x7fff);
});
@ -34,13 +34,13 @@ describe("ScrcpyFloatToInt16NumberType", () => {
const view = new Uint8Array(dataView.buffer);
dataView.setInt16(0, -0x8000, true);
expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(-1);
expect(ScrcpySignedFloatNumberVariant.deserialize(view, true)).toBe(-1);
dataView.setInt16(0, 0, true);
expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(0);
expect(ScrcpySignedFloatNumberVariant.deserialize(view, true)).toBe(0);
dataView.setInt16(0, 0x7fff, true);
expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(1);
expect(ScrcpySignedFloatNumberVariant.deserialize(view, true)).toBe(1);
});
});

View file

@ -1,5 +1,5 @@
import { getInt16 } from "@yume-chan/no-data-view";
import type { NumberFieldType } from "@yume-chan/struct";
import type { NumberFieldVariant } from "@yume-chan/struct";
import Struct, { NumberFieldDefinition } from "@yume-chan/struct";
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
@ -7,7 +7,7 @@ import { ScrcpyControlMessageType } from "../../control/index.js";
import type { ScrcpyScrollController } from "../1_16/index.js";
import { clamp } from "../1_16/index.js";
export const ScrcpyFloatToInt16NumberType: NumberFieldType = {
export const ScrcpySignedFloatNumberVariant: NumberFieldVariant = {
size: 2,
signed: true,
deserialize(array, littleEndian) {
@ -23,8 +23,8 @@ export const ScrcpyFloatToInt16NumberType: NumberFieldType = {
},
};
const ScrcpyFloatToInt16FieldDefinition = new NumberFieldDefinition(
ScrcpyFloatToInt16NumberType,
const ScrcpySignedFloatFieldDefinition = new NumberFieldDefinition(
ScrcpySignedFloatNumberVariant,
);
export const ScrcpyInjectScrollControlMessage1_25 = new Struct()
@ -33,8 +33,8 @@ export const ScrcpyInjectScrollControlMessage1_25 = new Struct()
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("scrollX", ScrcpyFloatToInt16FieldDefinition)
.field("scrollY", ScrcpyFloatToInt16FieldDefinition)
.field("scrollX", ScrcpySignedFloatFieldDefinition)
.field("scrollY", ScrcpySignedFloatFieldDefinition)
.int32("buttons");
export type ScrcpyInjectScrollControlMessage1_25 =

View file

@ -14,8 +14,8 @@ import type {
import {
CodecOptions,
ScrcpyFloatToUint16FieldDefinition,
ScrcpyOptions1_16,
ScrcpyUnsignedFloatFieldDefinition,
} from "./1_16/index.js";
import { ScrcpyOptions1_21 } from "./1_21.js";
import type { ScrcpyOptionsInit1_24 } from "./1_24.js";
@ -38,7 +38,7 @@ export const ScrcpyInjectTouchControlMessage2_0 = new Struct()
.uint32("pointerY")
.uint16("screenWidth")
.uint16("screenHeight")
.field("pressure", ScrcpyFloatToUint16FieldDefinition)
.field("pressure", ScrcpyUnsignedFloatFieldDefinition)
.uint32("actionButton")
.uint32("buttons");
@ -107,6 +107,58 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
ScrcpyOptionsInit2_0,
ScrcpyOptions1_25
> {
static async parseAudioMetadata(
stream: ReadableStream<Uint8Array>,
sendCodecMeta: boolean,
mapMetadata: (value: number) => ScrcpyAudioCodec,
getOptionCodec: () => ScrcpyAudioCodec,
): Promise<ScrcpyAudioStreamMetadata> {
const buffered = new BufferedReadableStream(stream);
const buffer = await buffered.readExactly(4);
// Treat it as a 32-bit number for simpler comparisons
const codecMetadataValue = getUint32BigEndian(buffer, 0);
// Server will send `0x00_00_00_00` and `0x00_00_00_01` even if `sendCodecMeta` is false
switch (codecMetadataValue) {
case 0x00_00_00_00:
return {
type: "disabled",
};
case 0x00_00_00_01:
return {
type: "errored",
};
}
if (sendCodecMeta) {
return {
type: "success",
codec: mapMetadata(codecMetadataValue),
stream: buffered.release(),
};
}
return {
type: "success",
// Infer codec from `audioCodec` option
codec: getOptionCodec(),
stream: new PushReadableStream<Uint8Array>(async (controller) => {
// Put the first 4 bytes back
await controller.enqueue(buffer);
const stream = buffered.release();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await controller.enqueue(value);
}
}),
};
}
static readonly DEFAULTS = {
...omit(ScrcpyOptions1_24.DEFAULTS, [
"bitRate",
@ -255,81 +307,34 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
override parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
return (async (): Promise<ScrcpyAudioStreamMetadata> => {
const buffered = new BufferedReadableStream(stream);
const buffer = await buffered.readExactly(4);
const codecMetadataValue = getUint32BigEndian(buffer, 0);
// Server will send `0x00_00_00_00` and `0x00_00_00_01` even if `sendCodecMeta` is false
switch (codecMetadataValue) {
case 0x00_00_00_00:
return {
type: "disabled",
};
case 0x00_00_00_01:
return {
type: "errored",
};
}
if (this.value.sendCodecMeta) {
let codec: ScrcpyAudioCodec;
switch (codecMetadataValue) {
case ScrcpyAudioCodec.OPUS.metadataValue:
codec = ScrcpyAudioCodec.OPUS;
break;
case ScrcpyAudioCodec.AAC.metadataValue:
codec = ScrcpyAudioCodec.AAC;
break;
return ScrcpyOptions2_0.parseAudioMetadata(
stream,
this.value.sendCodecMeta,
(value) => {
switch (value) {
case ScrcpyAudioCodec.RAW.metadataValue:
codec = ScrcpyAudioCodec.RAW;
break;
return ScrcpyAudioCodec.RAW;
case ScrcpyAudioCodec.OPUS.metadataValue:
return ScrcpyAudioCodec.OPUS;
case ScrcpyAudioCodec.AAC.metadataValue:
return ScrcpyAudioCodec.AAC;
default:
throw new Error(
`Unknown audio codec metadata value: ${codecMetadataValue}`,
`Unknown audio codec metadata value: ${value}`,
);
}
return {
type: "success",
codec,
stream: buffered.release(),
};
}
// Infer codec from `audioCodec` option
let codec: ScrcpyAudioCodec;
},
() => {
switch (this.value.audioCodec) {
case "opus":
codec = ScrcpyAudioCodec.OPUS;
break;
case "aac":
codec = ScrcpyAudioCodec.AAC;
break;
case "raw":
codec = ScrcpyAudioCodec.RAW;
break;
}
return {
type: "success",
codec,
stream: new PushReadableStream<Uint8Array>(
async (controller) => {
// Put the first 4 bytes back
await controller.enqueue(buffer);
const stream = buffered.release();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await controller.enqueue(value);
return ScrcpyAudioCodec.RAW;
case "opus":
return ScrcpyAudioCodec.OPUS;
case "aac":
return ScrcpyAudioCodec.AAC;
}
},
),
};
})();
);
}
override serializeInjectTouchControlMessage(

View file

@ -1,11 +1,14 @@
import type { ReadableStream } from "@yume-chan/stream-extra";
import { ScrcpyOptions1_21 } from "./1_21.js";
import { omit } from "./2_0.js";
import { ScrcpyOptions2_0, omit } from "./2_0.js";
import { ScrcpyOptions2_2, type ScrcpyOptionsInit2_2 } from "./2_2.js";
import { ScrcpyOptionsBase } from "./types.js";
import type { ValueOrPromise } from "@yume-chan/struct";
import { ScrcpyAudioCodec, type ScrcpyAudioStreamMetadata } from "./codec.js";
export interface ScrcpyOptionsInit2_3
extends Omit<ScrcpyOptionsInit2_2, "audioCodec"> {
audioCodec?: "raw" | "opus" | "aac" | "flac" | undefined;
audioCodec?: "raw" | "opus" | "aac" | "flac";
}
export class ScrcpyOptions2_3 extends ScrcpyOptionsBase<
@ -30,4 +33,41 @@ export class ScrcpyOptions2_3 extends ScrcpyOptionsBase<
override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults);
}
override parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> {
return ScrcpyOptions2_0.parseAudioMetadata(
stream,
this.value.sendCodecMeta,
(value) => {
switch (value) {
case ScrcpyAudioCodec.RAW.metadataValue:
return ScrcpyAudioCodec.RAW;
case ScrcpyAudioCodec.OPUS.metadataValue:
return ScrcpyAudioCodec.OPUS;
case ScrcpyAudioCodec.AAC.metadataValue:
return ScrcpyAudioCodec.AAC;
case ScrcpyAudioCodec.FLAC.metadataValue:
return ScrcpyAudioCodec.FLAC;
default:
throw new Error(
`Unknown audio codec metadata value: ${value}`,
);
}
},
() => {
switch (this.value.audioCodec) {
case "raw":
return ScrcpyAudioCodec.RAW;
case "opus":
return ScrcpyAudioCodec.OPUS;
case "aac":
return ScrcpyAudioCodec.AAC;
case "flac":
return ScrcpyAudioCodec.FLAC;
}
},
);
}
}

View file

@ -1,4 +1,5 @@
import { PromiseResolver } from "@yume-chan/async";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import type { ReadableStreamDefaultController } from "./stream.js";
import { ReadableStream, WritableStream } from "./stream.js";
@ -83,7 +84,8 @@ export interface ConcatBufferReadableStream
* If you want to decode the result as string,
* prefer `.pipeThrough(new DecodeUtf8Stream()).pipeThrough(new ConcatStringStream())`,
* than `.pipeThough(new ConcatBufferStream()).pipeThrough(new DecodeUtf8Stream())`,
* because concatenating strings is faster than concatenating `Uint8Array`s.
* because of JavaScript engine optimizations,
* concatenating strings is faster than concatenating `Uint8Array`s.
*/
export class ConcatBufferStream {
#segments: Uint8Array[] = [];
@ -99,7 +101,7 @@ export class ConcatBufferStream {
let offset = 0;
switch (this.#segments.length) {
case 0:
result = new Uint8Array(0);
result = EMPTY_UINT8_ARRAY;
break;
case 1:
result = this.#segments[0]!;

View file

@ -7,30 +7,9 @@ import type {
} from "./stream.js";
import {
WritableStream as NativeWritableStream,
ReadableStream,
ReadableStream as NativeReadableStream,
} from "./stream.js";
interface Task {
run<T>(callback: () => T): T;
}
interface Console {
createTask(name: string): Task;
}
interface GlobalExtension {
console: Console;
}
// `createTask` allows browser DevTools to track the call stack across async boundaries.
const { console } = globalThis as unknown as GlobalExtension;
const createTask: Console["createTask"] =
console.createTask?.bind(console) ??
(() => ({
run(callback) {
return callback();
},
}));
import { createTask, type Task } from "./task.js";
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return typeof value === "object" && value !== null && "then" in value;
@ -148,25 +127,24 @@ export namespace Consumable {
);
}
}
}
export interface ConsumableReadableStreamController<T> {
export interface ReadableStreamController<T> {
enqueue(chunk: T): Promise<void>;
close(): void;
error(reason: unknown): void;
}
export interface ConsumableReadableStreamSource<T> {
export interface ReadableStreamSource<T> {
start?(
controller: ConsumableReadableStreamController<T>,
controller: ReadableStreamController<T>,
): void | PromiseLike<void>;
pull?(
controller: ConsumableReadableStreamController<T>,
controller: ReadableStreamController<T>,
): void | PromiseLike<void>;
cancel?(reason: unknown): void | PromiseLike<void>;
}
export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
export class ReadableStream<T> extends NativeReadableStream<Consumable<T>> {
static async enqueue<T>(
controller: { enqueue: (chunk: Consumable<T>) => void },
chunk: T,
@ -177,12 +155,10 @@ export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
}
constructor(
source: ConsumableReadableStreamSource<T>,
source: ReadableStreamSource<T>,
strategy?: QueuingStrategy<T>,
) {
let wrappedController:
| ConsumableReadableStreamController<T>
| undefined;
let wrappedController: ReadableStreamController<T> | undefined;
let wrappedStrategy: QueuingStrategy<Consumable<T>> | undefined;
if (strategy) {
@ -202,10 +178,7 @@ export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
async start(controller) {
wrappedController = {
async enqueue(chunk) {
await ConsumableReadableStream.enqueue(
controller,
chunk,
);
await ReadableStream.enqueue(controller, chunk);
},
close() {
controller.close();
@ -228,3 +201,4 @@ export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
);
}
}
}

View file

@ -1,6 +1,6 @@
import { describe, expect, it, jest } from "@jest/globals";
import { ConsumableReadableStream } from "./consumable.js";
import { Consumable } from "./consumable.js";
import { DistributionStream } from "./distribution.js";
import { MaybeConsumable } from "./maybe-consumable.js";
@ -17,7 +17,7 @@ async function testInputOutput(
const write = jest.fn((chunk: Uint8Array) => {
void chunk;
});
await new ConsumableReadableStream<Uint8Array>({
await new Consumable.ReadableStream<Uint8Array>({
async start(controller) {
let offset = 0;
for (const length of inputLengths) {

View file

@ -1,4 +1,4 @@
import { ConsumableReadableStream } from "./consumable.js";
import { Consumable } from "./consumable.js";
import { MaybeConsumable } from "./maybe-consumable.js";
import { TransformStream } from "./stream.js";
@ -90,7 +90,7 @@ export class DistributionStream extends TransformStream<
await MaybeConsumable.tryConsume(chunk, async (chunk) => {
if (combiner) {
for (const buffer of combiner.push(chunk)) {
await ConsumableReadableStream.enqueue(
await Consumable.ReadableStream.enqueue(
controller,
buffer,
);
@ -100,7 +100,7 @@ export class DistributionStream extends TransformStream<
let available = chunk.byteLength;
while (available > 0) {
const end = offset + size;
await ConsumableReadableStream.enqueue(
await Consumable.ReadableStream.enqueue(
controller,
chunk.subarray(offset, end),
);

View file

@ -13,5 +13,6 @@ export * from "./split-string.js";
export * from "./stream.js";
export * from "./struct-deserialize.js";
export * from "./struct-serialize.js";
export * from "./task.js";
export * from "./wrap-readable.js";
export * from "./wrap-writable.js";

View file

@ -0,0 +1,21 @@
export interface Task {
run<T>(callback: () => T): T;
}
interface Console {
createTask(name: string): Task;
}
interface GlobalExtension {
console?: Console;
}
// `createTask` allows browser DevTools to track the call stack across async boundaries.
const global = globalThis as unknown as GlobalExtension;
export const createTask: (name: string) => Task =
global.console?.createTask?.bind(global.console) ??
(() => ({
run(callback) {
return callback();
},
}));

View file

@ -13,11 +13,11 @@ import type { ValueOrPromise } from "./utils.js";
import {
BigIntFieldDefinition,
BigIntFieldType,
BufferFieldSubType,
BigIntFieldVariant,
BufferFieldConverter,
FixedLengthBufferLikeFieldDefinition,
NumberFieldDefinition,
NumberFieldType,
NumberFieldVariant,
VariableLengthBufferLikeFieldDefinition,
} from "./index.js";
@ -120,7 +120,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int8);
expect(definition.variant).toBe(NumberFieldVariant.Int8);
});
it("`uint8` should append an `uint8` field", () => {
@ -130,7 +130,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint8);
expect(definition.variant).toBe(NumberFieldVariant.Uint8);
});
it("`int16` should append an `int16` field", () => {
@ -140,7 +140,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int16);
expect(definition.variant).toBe(NumberFieldVariant.Int16);
});
it("`uint16` should append an `uint16` field", () => {
@ -150,7 +150,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint16);
expect(definition.variant).toBe(NumberFieldVariant.Uint16);
});
it("`int32` should append an `int32` field", () => {
@ -160,7 +160,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Int32);
expect(definition.variant).toBe(NumberFieldVariant.Int32);
});
it("`uint32` should append an `uint32` field", () => {
@ -170,7 +170,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as NumberFieldDefinition;
expect(definition).toBeInstanceOf(NumberFieldDefinition);
expect(definition.type).toBe(NumberFieldType.Uint32);
expect(definition.variant).toBe(NumberFieldVariant.Uint32);
});
it("`int64` should append an `int64` field", () => {
@ -180,7 +180,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as BigIntFieldDefinition;
expect(definition).toBeInstanceOf(BigIntFieldDefinition);
expect(definition.type).toBe(BigIntFieldType.Int64);
expect(definition.variant).toBe(BigIntFieldVariant.Int64);
});
it("`uint64` should append an `uint64` field", () => {
@ -190,7 +190,7 @@ describe("Struct", () => {
const definition = struct.fields[0]![1] as BigIntFieldDefinition;
expect(definition).toBeInstanceOf(BigIntFieldDefinition);
expect(definition.type).toBe(BigIntFieldType.Uint64);
expect(definition.variant).toBe(BigIntFieldVariant.Uint64);
});
describe("#uint8ArrayLike", () => {
@ -205,7 +205,9 @@ describe("Struct", () => {
expect(definition).toBeInstanceOf(
FixedLengthBufferLikeFieldDefinition,
);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.converter).toBeInstanceOf(
BufferFieldConverter,
);
expect(definition.options.length).toBe(10);
});
@ -219,7 +221,9 @@ describe("Struct", () => {
expect(definition).toBeInstanceOf(
FixedLengthBufferLikeFieldDefinition,
);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.converter).toBeInstanceOf(
BufferFieldConverter,
);
expect(definition.options.length).toBe(10);
});
});
@ -237,7 +241,9 @@ describe("Struct", () => {
expect(definition).toBeInstanceOf(
VariableLengthBufferLikeFieldDefinition,
);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.converter).toBeInstanceOf(
BufferFieldConverter,
);
expect(definition.options.lengthField).toBe("barLength");
});
@ -253,7 +259,9 @@ describe("Struct", () => {
expect(definition).toBeInstanceOf(
VariableLengthBufferLikeFieldDefinition,
);
expect(definition.type).toBeInstanceOf(BufferFieldSubType);
expect(definition.converter).toBeInstanceOf(
BufferFieldConverter,
);
expect(definition.options.lengthField).toBe("barLength");
});
});
@ -270,19 +278,31 @@ describe("Struct", () => {
const field0 = struct.fields[0]!;
expect(field0).toHaveProperty("0", "int8");
expect(field0[1]).toHaveProperty("type", NumberFieldType.Int8);
expect(field0[1]).toHaveProperty(
"variant",
NumberFieldVariant.Int8,
);
const field1 = struct.fields[1]!;
expect(field1).toHaveProperty("0", "int16");
expect(field1[1]).toHaveProperty("type", NumberFieldType.Int16);
expect(field1[1]).toHaveProperty(
"variant",
NumberFieldVariant.Int16,
);
const field2 = struct.fields[2]!;
expect(field2).toHaveProperty("0", "int32");
expect(field2[1]).toHaveProperty("type", NumberFieldType.Int32);
expect(field2[1]).toHaveProperty(
"variant",
NumberFieldVariant.Int32,
);
const field3 = struct.fields[3]!;
expect(field3).toHaveProperty("0", "int64");
expect(field3[1]).toHaveProperty("type", BigIntFieldType.Int64);
expect(field3[1]).toHaveProperty(
"variant",
BigIntFieldVariant.Int64,
);
});
});

View file

@ -16,19 +16,19 @@ import {
} from "./basic/index.js";
import { SyncPromise } from "./sync-promise.js";
import type {
BufferFieldSubType,
BufferFieldConverter,
FixedLengthBufferLikeFieldOptions,
LengthField,
VariableLengthBufferLikeFieldOptions,
} from "./types/index.js";
import {
BigIntFieldDefinition,
BigIntFieldType,
BigIntFieldVariant,
FixedLengthBufferLikeFieldDefinition,
NumberFieldDefinition,
NumberFieldType,
StringBufferFieldSubType,
Uint8ArrayBufferFieldSubType,
NumberFieldVariant,
StringBufferFieldConverter,
Uint8ArrayBufferFieldConverter,
VariableLengthBufferLikeFieldDefinition,
} from "./types/index.js";
import type { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils.js";
@ -86,7 +86,7 @@ interface ArrayBufferLikeFieldCreator<
*/
<
TName extends PropertyKey,
TType extends BufferFieldSubType<unknown, unknown>,
TType extends BufferFieldConverter<unknown, unknown>,
TTypeScriptType = TType["TTypeScriptType"],
>(
name: TName,
@ -110,7 +110,7 @@ interface ArrayBufferLikeFieldCreator<
*/
<
TName extends PropertyKey,
TType extends BufferFieldSubType<unknown, unknown>,
TType extends BufferFieldConverter<unknown, unknown>,
TOptions extends VariableLengthBufferLikeFieldOptions<TFields>,
TTypeScriptType = TType["TTypeScriptType"],
>(
@ -136,7 +136,7 @@ interface BoundArrayBufferLikeFieldDefinitionCreator<
TOmitInitKey extends PropertyKey,
TExtra extends object,
TPostDeserialized,
TType extends BufferFieldSubType<unknown, unknown>,
TType extends BufferFieldConverter<unknown, unknown>,
> {
<TName extends PropertyKey, TTypeScriptType = TType["TTypeScriptType"]>(
name: TName,
@ -351,7 +351,7 @@ export class Struct<
#number<
TName extends PropertyKey,
TType extends NumberFieldType = NumberFieldType,
TType extends NumberFieldVariant = NumberFieldVariant,
TTypeScriptType = number,
>(name: TName, type: TType, typeScriptType?: TTypeScriptType) {
return this.field(
@ -367,7 +367,7 @@ export class Struct<
name: TName,
typeScriptType?: TTypeScriptType,
) {
return this.#number(name, NumberFieldType.Int8, typeScriptType);
return this.#number(name, NumberFieldVariant.Int8, typeScriptType);
}
/**
@ -377,7 +377,7 @@ export class Struct<
name: TName,
typeScriptType?: TTypeScriptType,
) {
return this.#number(name, NumberFieldType.Uint8, typeScriptType);
return this.#number(name, NumberFieldVariant.Uint8, typeScriptType);
}
/**
@ -387,7 +387,7 @@ export class Struct<
name: TName,
typeScriptType?: TTypeScriptType,
) {
return this.#number(name, NumberFieldType.Int16, typeScriptType);
return this.#number(name, NumberFieldVariant.Int16, typeScriptType);
}
/**
@ -397,7 +397,7 @@ export class Struct<
name: TName,
typeScriptType?: TTypeScriptType,
) {
return this.#number(name, NumberFieldType.Uint16, typeScriptType);
return this.#number(name, NumberFieldVariant.Uint16, typeScriptType);
}
/**
@ -407,7 +407,7 @@ export class Struct<
name: TName,
typeScriptType?: TTypeScriptType,
) {
return this.#number(name, NumberFieldType.Int32, typeScriptType);
return this.#number(name, NumberFieldVariant.Int32, typeScriptType);
}
/**
@ -417,12 +417,12 @@ export class Struct<
name: TName,
typeScriptType?: TTypeScriptType,
) {
return this.#number(name, NumberFieldType.Uint32, typeScriptType);
return this.#number(name, NumberFieldVariant.Uint32, typeScriptType);
}
#bigint<
TName extends PropertyKey,
TType extends BigIntFieldType = BigIntFieldType,
TType extends BigIntFieldVariant = BigIntFieldVariant,
TTypeScriptType = TType["TTypeScriptType"],
>(name: TName, type: TType, typeScriptType?: TTypeScriptType) {
return this.field(
@ -438,9 +438,9 @@ export class Struct<
*/
int64<
TName extends PropertyKey,
TTypeScriptType = BigIntFieldType["TTypeScriptType"],
TTypeScriptType = BigIntFieldVariant["TTypeScriptType"],
>(name: TName, typeScriptType?: TTypeScriptType) {
return this.#bigint(name, BigIntFieldType.Int64, typeScriptType);
return this.#bigint(name, BigIntFieldVariant.Int64, typeScriptType);
}
/**
@ -450,9 +450,9 @@ export class Struct<
*/
uint64<
TName extends PropertyKey,
TTypeScriptType = BigIntFieldType["TTypeScriptType"],
TTypeScriptType = BigIntFieldVariant["TTypeScriptType"],
>(name: TName, typeScriptType?: TTypeScriptType) {
return this.#bigint(name, BigIntFieldType.Uint64, typeScriptType);
return this.#bigint(name, BigIntFieldVariant.Uint64, typeScriptType);
}
#arrayBufferLike: ArrayBufferLikeFieldCreator<
@ -462,7 +462,7 @@ export class Struct<
TPostDeserialized
> = (
name: PropertyKey,
type: BufferFieldSubType,
type: BufferFieldConverter,
options:
| FixedLengthBufferLikeFieldOptions
| VariableLengthBufferLikeFieldOptions,
@ -485,7 +485,7 @@ export class Struct<
TOmitInitKey,
TExtra,
TPostDeserialized,
Uint8ArrayBufferFieldSubType
Uint8ArrayBufferFieldConverter
> = (
name: PropertyKey,
options: unknown,
@ -493,7 +493,7 @@ export class Struct<
): never => {
return this.#arrayBufferLike(
name,
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
options as never,
typeScriptType,
) as never;
@ -504,7 +504,7 @@ export class Struct<
TOmitInitKey,
TExtra,
TPostDeserialized,
StringBufferFieldSubType
StringBufferFieldConverter
> = (
name: PropertyKey,
options: unknown,
@ -512,7 +512,7 @@ export class Struct<
): never => {
return this.#arrayBufferLike(
name,
StringBufferFieldSubType.Instance,
StringBufferFieldConverter.Instance,
options as never,
typeScriptType,
) as never;

View file

@ -14,53 +14,57 @@ import { StructFieldDefinition, StructFieldValue } from "../basic/index.js";
import { SyncPromise } from "../sync-promise.js";
import type { ValueOrPromise } from "../utils.js";
type GetBigInt64 = (
export type BigIntDeserializer = (
array: Uint8Array,
byteOffset: number,
littleEndian: boolean,
) => bigint;
type SetBigInt64 = (
export type BigIntSerializer = (
array: Uint8Array,
byteOffset: number,
value: bigint,
littleEndian: boolean,
) => void;
export class BigIntFieldType {
export class BigIntFieldVariant {
readonly TTypeScriptType!: bigint;
readonly size: number;
readonly getter: GetBigInt64;
readonly deserialize: BigIntDeserializer;
readonly setter: SetBigInt64;
readonly serialize: BigIntSerializer;
constructor(size: number, getter: GetBigInt64, setter: SetBigInt64) {
constructor(
size: number,
deserialize: BigIntDeserializer,
serialize: BigIntSerializer,
) {
this.size = size;
this.getter = getter;
this.setter = setter;
this.deserialize = deserialize;
this.serialize = serialize;
}
static readonly Int64 = new BigIntFieldType(8, getInt64, setInt64);
static readonly Int64 = new BigIntFieldVariant(8, getInt64, setInt64);
static readonly Uint64 = new BigIntFieldType(8, getUint64, setUint64);
static readonly Uint64 = new BigIntFieldVariant(8, getUint64, setUint64);
}
export class BigIntFieldDefinition<
TType extends BigIntFieldType = BigIntFieldType,
TTypeScriptType = TType["TTypeScriptType"],
TVariant extends BigIntFieldVariant = BigIntFieldVariant,
TTypeScriptType = TVariant["TTypeScriptType"],
> extends StructFieldDefinition<void, TTypeScriptType> {
readonly type: TType;
readonly variant: TVariant;
constructor(type: TType, typescriptType?: TTypeScriptType) {
constructor(variant: TVariant, typescriptType?: TTypeScriptType) {
void typescriptType;
super();
this.type = type;
this.variant = variant;
}
getSize(): number {
return this.type.size;
return this.variant.size;
}
create(
@ -90,7 +94,11 @@ export class BigIntFieldDefinition<
return stream.readExactly(this.getSize());
})
.then((array) => {
const value = this.type.getter(array, 0, options.littleEndian);
const value = this.variant.deserialize(
array,
0,
options.littleEndian,
);
return this.create(options, struct, value as never);
})
.valueOrPromise();
@ -98,14 +106,14 @@ export class BigIntFieldDefinition<
}
export class BigIntFieldValue<
TDefinition extends BigIntFieldDefinition<BigIntFieldType, unknown>,
TDefinition extends BigIntFieldDefinition<BigIntFieldVariant, unknown>,
> extends StructFieldValue<TDefinition> {
override serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
): void {
this.definition.type.setter(
this.definition.variant.serialize(
array,
offset,
this.value as never,

View file

@ -3,12 +3,12 @@ import { describe, expect, it, jest } from "@jest/globals";
import type { ExactReadable } from "../../basic/index.js";
import { StructDefaultOptions, StructValue } from "../../basic/index.js";
import type { BufferFieldSubType } from "./base.js";
import type { BufferFieldConverter } from "./base.js";
import {
BufferLikeFieldDefinition,
EMPTY_UINT8_ARRAY,
StringBufferFieldSubType,
Uint8ArrayBufferFieldSubType,
StringBufferFieldConverter,
Uint8ArrayBufferFieldConverter,
} from "./base.js";
class MockDeserializationStream implements ExactReadable {
@ -23,37 +23,37 @@ describe("Types", () => {
describe("Buffer", () => {
describe("Uint8ArrayBufferFieldSubType", () => {
it("should have a static instance", () => {
expect(Uint8ArrayBufferFieldSubType.Instance).toBeInstanceOf(
Uint8ArrayBufferFieldSubType,
expect(Uint8ArrayBufferFieldConverter.Instance).toBeInstanceOf(
Uint8ArrayBufferFieldConverter,
);
});
it("`#toBuffer` should return the same `Uint8Array`", () => {
const array = new Uint8Array(10);
expect(
Uint8ArrayBufferFieldSubType.Instance.toBuffer(array),
Uint8ArrayBufferFieldConverter.Instance.toBuffer(array),
).toBe(array);
});
it("`#fromBuffer` should return the same `Uint8Array`", () => {
const buffer = new Uint8Array(10);
expect(
Uint8ArrayBufferFieldSubType.Instance.toValue(buffer),
Uint8ArrayBufferFieldConverter.Instance.toValue(buffer),
).toBe(buffer);
});
it("`#getSize` should return the `byteLength` of the `Uint8Array`", () => {
const array = new Uint8Array(10);
expect(
Uint8ArrayBufferFieldSubType.Instance.getSize(array),
Uint8ArrayBufferFieldConverter.Instance.getSize(array),
).toBe(10);
});
});
describe("StringBufferFieldSubType", () => {
it("should have a static instance", () => {
expect(StringBufferFieldSubType.Instance).toBeInstanceOf(
StringBufferFieldSubType,
expect(StringBufferFieldConverter.Instance).toBeInstanceOf(
StringBufferFieldConverter,
);
});
@ -61,25 +61,27 @@ describe("Types", () => {
const text = "foo";
const array = new Uint8Array(Buffer.from(text, "utf-8"));
expect(
StringBufferFieldSubType.Instance.toBuffer(text),
StringBufferFieldConverter.Instance.toBuffer(text),
).toEqual(array);
});
it("`#fromBuffer` should return the encoded ArrayBuffer", () => {
const text = "foo";
const array = new Uint8Array(Buffer.from(text, "utf-8"));
expect(StringBufferFieldSubType.Instance.toValue(array)).toBe(
expect(StringBufferFieldConverter.Instance.toValue(array)).toBe(
text,
);
});
it("`#getSize` should return -1", () => {
expect(StringBufferFieldSubType.Instance.getSize()).toBe(-1);
expect(StringBufferFieldConverter.Instance.getSize()).toBe(
undefined,
);
});
});
class MockArrayBufferFieldDefinition<
TType extends BufferFieldSubType,
TType extends BufferFieldConverter,
> extends BufferLikeFieldDefinition<TType, number> {
getSize(): number {
return this.options;
@ -90,7 +92,7 @@ describe("Types", () => {
it("should work with `Uint8ArrayBufferFieldSubType`", () => {
const size = 10;
const definition = new MockArrayBufferFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
size,
);
@ -114,7 +116,7 @@ describe("Types", () => {
it("should work when `#getSize` returns `0`", () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
size,
);
@ -143,7 +145,7 @@ describe("Types", () => {
it("should clear `array` field", () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
size,
);
@ -169,7 +171,7 @@ describe("Types", () => {
it("should be able to serialize with cached `array`", () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
size,
);
@ -197,7 +199,7 @@ describe("Types", () => {
it("should be able to serialize a modified value", () => {
const size = 0;
const definition = new MockArrayBufferFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
size,
);

View file

@ -11,46 +11,49 @@ import type { ValueOrPromise } from "../../utils.js";
import { decodeUtf8, encodeUtf8 } from "../../utils.js";
/**
* Base class for all types that
* can be converted from an `Uint8Array` when deserialized,
* and need to be converted to an `Uint8Array` when serializing
* A converter for buffer-like fields.
* It converts `Uint8Array`s to custom-typed values when deserializing,
* and convert values back to `Uint8Array`s when serializing.
*
* @template TValue The actual TypeScript type of this type
* @template TTypeScriptType Optional another type (should be compatible with `TType`)
* specified by user when creating field definitions.
* @template TValue The type of the value that the converter converts to/from `Uint8Array`.
* @template TTypeScriptType Optionally another type to refine `TValue`.
* For example, `TValue` is `string`, and `TTypeScriptType` is `"foo" | "bar"`.
* `TValue` is specified by the developer when creating an converter implementation,
* `TTypeScriptType` is specified by the user when creating a field.
*/
export abstract class BufferFieldSubType<
export abstract class BufferFieldConverter<
TValue = unknown,
TTypeScriptType = TValue,
> {
readonly TTypeScriptType!: TTypeScriptType;
/**
* When implemented in derived classes, converts the type-specific `value` to an `Uint8Array`
* When implemented in derived classes, converts the custom `value` to an `Uint8Array`
*
* This function should be "pure", i.e.,
* same `value` should always be converted to `Uint8Array`s that have same content.
*/
abstract toBuffer(value: TValue): Uint8Array;
/** When implemented in derived classes, converts the `Uint8Array` to a type-specific value */
/** When implemented in derived classes, converts the `Uint8Array` to a custom value */
abstract toValue(array: Uint8Array): TValue;
/**
* When implemented in derived classes, gets the size in byte of the type-specific `value`.
* When implemented in derived classes, gets the size in byte of the custom `value`.
*
* If the size can't be calculated without first converting the `value` back to an `Uint8Array`,
* implementer can returns `-1`, so the caller will get its size by first converting it to
* an `Uint8Array` (and cache the result).
* If the size can't be determined without first converting the `value` back to an `Uint8Array`,
* the implementer should return `undefined`. In which case, the caller will call `toBuffer` to
* convert the value to a `Uint8Array`, then read the length of the `Uint8Array`. The caller can
* cache the result so the serialization process doesn't need to call `toBuffer` again.
*/
abstract getSize(value: TValue): number;
abstract getSize(value: TValue): number | undefined;
}
/** An `BufferFieldSubType` that's actually an `Uint8Array` */
export class Uint8ArrayBufferFieldSubType<
/** An identity converter, doesn't convert to anything else. */
export class Uint8ArrayBufferFieldConverter<
TTypeScriptType = Uint8Array,
> extends BufferFieldSubType<Uint8Array, TTypeScriptType> {
static readonly Instance = new Uint8ArrayBufferFieldSubType();
> extends BufferFieldConverter<Uint8Array, TTypeScriptType> {
static readonly Instance = new Uint8ArrayBufferFieldConverter();
protected constructor() {
super();
@ -65,15 +68,15 @@ export class Uint8ArrayBufferFieldSubType<
}
getSize(value: Uint8Array): number {
return value.byteLength;
return value.length;
}
}
/** An `BufferFieldSubType` that converts between `Uint8Array` and `string` */
export class StringBufferFieldSubType<
export class StringBufferFieldConverter<
TTypeScriptType = string,
> extends BufferFieldSubType<string, TTypeScriptType> {
static readonly Instance = new StringBufferFieldSubType();
> extends BufferFieldConverter<string, TTypeScriptType> {
static readonly Instance = new StringBufferFieldConverter();
toBuffer(value: string): Uint8Array {
return encodeUtf8(value);
@ -83,31 +86,29 @@ export class StringBufferFieldSubType<
return decodeUtf8(array);
}
getSize(): number {
// Return `-1`, so `BufferLikeFieldDefinition` will
// convert this `value` into an `Uint8Array` (and cache the result),
// Then get the size from that `Uint8Array`
return -1;
getSize(): number | undefined {
// See the note in `BufferFieldConverter.getSize`
return undefined;
}
}
export const EMPTY_UINT8_ARRAY = new Uint8Array(0);
export abstract class BufferLikeFieldDefinition<
TType extends BufferFieldSubType<any, any> = BufferFieldSubType<
TConverter extends BufferFieldConverter<
unknown,
unknown
>,
> = BufferFieldConverter<unknown, unknown>,
TOptions = void,
TOmitInitKey extends PropertyKey = never,
TTypeScriptType = TType["TTypeScriptType"],
TTypeScriptType = TConverter["TTypeScriptType"],
> extends StructFieldDefinition<TOptions, TTypeScriptType, TOmitInitKey> {
readonly type: TType;
readonly converter: TConverter;
readonly TTypeScriptType!: TTypeScriptType;
constructor(type: TType, options: TOptions) {
constructor(converter: TConverter, options: TOptions) {
super(options);
this.type = type;
this.converter = converter;
}
protected getDeserializeSize(struct: StructValue): number {
@ -151,7 +152,7 @@ export abstract class BufferLikeFieldDefinition<
}
})
.then((array) => {
const value = this.type.toValue(array) as TTypeScriptType;
const value = this.converter.toValue(array) as TTypeScriptType;
return this.create(options, struct, value, array);
})
.valueOrPromise();
@ -160,7 +161,7 @@ export abstract class BufferLikeFieldDefinition<
export class BufferLikeFieldValue<
TDefinition extends BufferLikeFieldDefinition<
BufferFieldSubType<any, any>,
BufferFieldConverter<unknown, unknown>,
any,
any,
any
@ -191,7 +192,7 @@ export class BufferLikeFieldValue<
array: Uint8Array,
offset: number,
): void {
this.array ??= this.definition.type.toBuffer(this.value);
this.array ??= this.definition.converter.toBuffer(this.value);
array.set(this.array, offset);
}
}

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from "@jest/globals";
import { Uint8ArrayBufferFieldSubType } from "./base.js";
import { Uint8ArrayBufferFieldConverter } from "./base.js";
import { FixedLengthBufferLikeFieldDefinition } from "./fixed-length.js";
describe("Types", () => {
@ -8,7 +8,7 @@ describe("Types", () => {
describe("#getSize", () => {
it("should return size in its options", () => {
const definition = new FixedLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ length: 10 },
);
expect(definition.getSize()).toBe(10);

View file

@ -1,4 +1,4 @@
import type { BufferFieldSubType } from "./base.js";
import type { BufferFieldConverter } from "./base.js";
import { BufferLikeFieldDefinition } from "./base.js";
export interface FixedLengthBufferLikeFieldOptions {
@ -6,11 +6,16 @@ export interface FixedLengthBufferLikeFieldOptions {
}
export class FixedLengthBufferLikeFieldDefinition<
TType extends BufferFieldSubType = BufferFieldSubType,
TConverter extends BufferFieldConverter = BufferFieldConverter,
TOptions extends
FixedLengthBufferLikeFieldOptions = FixedLengthBufferLikeFieldOptions,
TTypeScriptType = TType["TTypeScriptType"],
> extends BufferLikeFieldDefinition<TType, TOptions, never, TTypeScriptType> {
TTypeScriptType = TConverter["TTypeScriptType"],
> extends BufferLikeFieldDefinition<
TConverter,
TOptions,
never,
TTypeScriptType
> {
getSize(): number {
return this.options.length;
}

View file

@ -11,9 +11,9 @@ import {
} from "../../basic/index.js";
import {
BufferFieldSubType,
BufferFieldConverter,
EMPTY_UINT8_ARRAY,
Uint8ArrayBufferFieldSubType,
Uint8ArrayBufferFieldConverter,
} from "./base.js";
import {
VariableLengthBufferLikeFieldDefinition,
@ -334,7 +334,7 @@ describe("Types", () => {
const arrayBufferFieldDefinition =
new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -371,7 +371,7 @@ describe("Types", () => {
const arrayBufferFieldDefinition =
new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -409,7 +409,7 @@ describe("Types", () => {
const arrayBufferFieldDefinition =
new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -433,7 +433,7 @@ describe("Types", () => {
});
describe("#getSize", () => {
class MockArrayBufferFieldType extends BufferFieldSubType<Uint8Array> {
class MockArrayBufferFieldType extends BufferFieldConverter<Uint8Array> {
override toBuffer = jest.fn((value: Uint8Array): Uint8Array => {
return value;
});
@ -444,12 +444,14 @@ describe("Types", () => {
},
);
size = 0;
size: number | undefined = 0;
override getSize = jest.fn((value: Uint8Array): number => {
override getSize = jest.fn(
(value: Uint8Array): number | undefined => {
void value;
return this.size;
});
},
);
}
it("should return cached size if exist", () => {
@ -546,7 +548,7 @@ describe("Types", () => {
value,
);
arrayBufferFieldType.size = -1;
arrayBufferFieldType.size = undefined;
expect(bufferFieldValue.getSize()).toBe(100);
expect(arrayBufferFieldType.toValue).toHaveBeenCalledTimes(0);
expect(arrayBufferFieldType.toBuffer).toHaveBeenCalledTimes(1);
@ -566,7 +568,7 @@ describe("Types", () => {
const arrayBufferFieldDefinition =
new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -596,7 +598,7 @@ describe("Types", () => {
const arrayBufferFieldDefinition =
new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -622,7 +624,7 @@ describe("Types", () => {
describe("#getSize", () => {
it("should always return `0`", () => {
const definition = new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField: "foo" },
);
expect(definition.getSize()).toBe(0);
@ -638,7 +640,7 @@ describe("Types", () => {
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -660,7 +662,7 @@ describe("Types", () => {
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -683,7 +685,7 @@ describe("Types", () => {
const radix = 8;
const definition = new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField, lengthFieldRadix: radix },
);
@ -709,7 +711,7 @@ describe("Types", () => {
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);
@ -742,7 +744,7 @@ describe("Types", () => {
struct.set(lengthField, originalLengthFieldValue);
const definition = new VariableLengthBufferLikeFieldDefinition(
Uint8ArrayBufferFieldSubType.Instance,
Uint8ArrayBufferFieldConverter.Instance,
{ lengthField },
);

View file

@ -9,7 +9,7 @@ import type {
import { StructFieldValue } from "../../basic/index.js";
import type { KeysOfType } from "../../utils.js";
import type { BufferFieldSubType } from "./base.js";
import type { BufferFieldConverter } from "./base.js";
import { BufferLikeFieldDefinition, BufferLikeFieldValue } from "./base.js";
export type LengthField<TFields> = KeysOfType<TFields, number | string>;
@ -35,12 +35,12 @@ export interface VariableLengthBufferLikeFieldOptions<
}
export class VariableLengthBufferLikeFieldDefinition<
TType extends BufferFieldSubType = BufferFieldSubType,
TConverter extends BufferFieldConverter = BufferFieldConverter,
TOptions extends
VariableLengthBufferLikeFieldOptions = VariableLengthBufferLikeFieldOptions,
TTypeScriptType = TType["TTypeScriptType"],
TTypeScriptType = TConverter["TTypeScriptType"],
> extends BufferLikeFieldDefinition<
TType,
TConverter,
TOptions,
TOptions["lengthField"],
TTypeScriptType
@ -106,14 +106,24 @@ export class VariableLengthBufferLikeStructFieldValue<
}
override getSize() {
if (this.length === undefined) {
this.length = this.definition.type.getSize(this.value);
if (this.length === -1) {
this.array = this.definition.type.toBuffer(this.value);
this.length = this.array.byteLength;
}
if (this.length !== undefined) {
// Have cached length
return this.length;
}
this.length = this.definition.converter.getSize(this.value);
if (this.length !== undefined) {
if (this.length < 0) {
throw new Error("Invalid length");
}
// The converter knows the size
return this.length;
}
// The converter doesn't know the size, so we need to convert to buffer first
this.array = this.definition.converter.toBuffer(this.value);
this.length = this.array.byteLength;
return this.length;
}

View file

@ -3,10 +3,10 @@ import { describe, expect, it, jest, test } from "@jest/globals";
import type { ExactReadable } from "../basic/index.js";
import { StructDefaultOptions, StructValue } from "../basic/index.js";
import { NumberFieldDefinition, NumberFieldType } from "./number.js";
import { NumberFieldDefinition, NumberFieldVariant } from "./number.js";
function testEndian(
type: NumberFieldType,
type: NumberFieldVariant,
min: number,
max: number,
littleEndian: boolean,
@ -55,7 +55,7 @@ function testEndian(
});
}
function testDeserialize(type: NumberFieldType) {
function testDeserialize(type: NumberFieldVariant) {
if (type.size === 1) {
if (type.signed) {
const MIN = -(2 ** (type.size * 8 - 1));
@ -89,65 +89,65 @@ function testDeserialize(type: NumberFieldType) {
describe("Types", () => {
describe("Number", () => {
describe("NumberFieldType", () => {
describe("NumberFieldVariant", () => {
describe("Int8", () => {
const key = "Int8";
test("basic", () => {
expect(NumberFieldType[key]).toHaveProperty("size", 1);
expect(NumberFieldVariant[key]).toHaveProperty("size", 1);
});
testDeserialize(NumberFieldType[key]);
testDeserialize(NumberFieldVariant[key]);
});
describe("Uint8", () => {
const key = "Uint8";
test("basic", () => {
expect(NumberFieldType[key]).toHaveProperty("size", 1);
expect(NumberFieldVariant[key]).toHaveProperty("size", 1);
});
testDeserialize(NumberFieldType[key]);
testDeserialize(NumberFieldVariant[key]);
});
describe("Int16", () => {
const key = "Int16";
test("basic", () => {
expect(NumberFieldType[key]).toHaveProperty("size", 2);
expect(NumberFieldVariant[key]).toHaveProperty("size", 2);
});
testDeserialize(NumberFieldType[key]);
testDeserialize(NumberFieldVariant[key]);
});
describe("Uint16", () => {
const key = "Uint16";
test("basic", () => {
expect(NumberFieldType[key]).toHaveProperty("size", 2);
expect(NumberFieldVariant[key]).toHaveProperty("size", 2);
});
testDeserialize(NumberFieldType[key]);
testDeserialize(NumberFieldVariant[key]);
});
describe("Int32", () => {
const key = "Int32";
test("basic", () => {
expect(NumberFieldType[key]).toHaveProperty("size", 4);
expect(NumberFieldVariant[key]).toHaveProperty("size", 4);
});
testDeserialize(NumberFieldType[key]);
testDeserialize(NumberFieldVariant[key]);
});
describe("Uint32", () => {
const key = "Uint32";
test("basic", () => {
expect(NumberFieldType[key]).toHaveProperty("size", 4);
expect(NumberFieldVariant[key]).toHaveProperty("size", 4);
});
testDeserialize(NumberFieldType[key]);
testDeserialize(NumberFieldVariant[key]);
});
});
@ -156,32 +156,32 @@ describe("Types", () => {
it("should return size of its type", () => {
expect(
new NumberFieldDefinition(
NumberFieldType.Int8,
NumberFieldVariant.Int8,
).getSize(),
).toBe(1);
expect(
new NumberFieldDefinition(
NumberFieldType.Uint8,
NumberFieldVariant.Uint8,
).getSize(),
).toBe(1);
expect(
new NumberFieldDefinition(
NumberFieldType.Int16,
NumberFieldVariant.Int16,
).getSize(),
).toBe(2);
expect(
new NumberFieldDefinition(
NumberFieldType.Uint16,
NumberFieldVariant.Uint16,
).getSize(),
).toBe(2);
expect(
new NumberFieldDefinition(
NumberFieldType.Int32,
NumberFieldVariant.Int32,
).getSize(),
).toBe(4);
expect(
new NumberFieldDefinition(
NumberFieldType.Uint32,
NumberFieldVariant.Uint32,
).getSize(),
).toBe(4);
});
@ -195,7 +195,7 @@ describe("Types", () => {
const stream: ExactReadable = { position: 0, readExactly };
const definition = new NumberFieldDefinition(
NumberFieldType.Uint8,
NumberFieldVariant.Uint8,
);
const struct = new StructValue({});
const value = definition.deserialize(
@ -207,7 +207,7 @@ describe("Types", () => {
expect(value.get()).toBe(1);
expect(readExactly).toHaveBeenCalledTimes(1);
expect(readExactly).toHaveBeenCalledWith(
NumberFieldType.Uint8.size,
NumberFieldVariant.Uint8.size,
);
});
@ -218,7 +218,7 @@ describe("Types", () => {
const stream: ExactReadable = { position: 0, readExactly };
const definition = new NumberFieldDefinition(
NumberFieldType.Uint16,
NumberFieldVariant.Uint16,
);
const struct = new StructValue({});
const value = definition.deserialize(
@ -230,7 +230,7 @@ describe("Types", () => {
expect(value.get()).toBe((1 << 8) | 2);
expect(readExactly).toHaveBeenCalledTimes(1);
expect(readExactly).toHaveBeenCalledWith(
NumberFieldType.Uint16.size,
NumberFieldVariant.Uint16.size,
);
});
@ -241,7 +241,7 @@ describe("Types", () => {
const stream: ExactReadable = { position: 0, readExactly };
const definition = new NumberFieldDefinition(
NumberFieldType.Uint16,
NumberFieldVariant.Uint16,
);
const struct = new StructValue({});
const value = definition.deserialize(
@ -253,7 +253,7 @@ describe("Types", () => {
expect(value.get()).toBe((2 << 8) | 1);
expect(readExactly).toHaveBeenCalledTimes(1);
expect(readExactly).toHaveBeenCalledWith(
NumberFieldType.Uint16.size,
NumberFieldVariant.Uint16.size,
);
});
});
@ -265,37 +265,37 @@ describe("Types", () => {
const struct = new StructValue({});
expect(
new NumberFieldDefinition(NumberFieldType.Int8)
new NumberFieldDefinition(NumberFieldVariant.Int8)
.create(StructDefaultOptions, struct, 42)
.getSize(),
).toBe(1);
expect(
new NumberFieldDefinition(NumberFieldType.Uint8)
new NumberFieldDefinition(NumberFieldVariant.Uint8)
.create(StructDefaultOptions, struct, 42)
.getSize(),
).toBe(1);
expect(
new NumberFieldDefinition(NumberFieldType.Int16)
new NumberFieldDefinition(NumberFieldVariant.Int16)
.create(StructDefaultOptions, struct, 42)
.getSize(),
).toBe(2);
expect(
new NumberFieldDefinition(NumberFieldType.Uint16)
new NumberFieldDefinition(NumberFieldVariant.Uint16)
.create(StructDefaultOptions, struct, 42)
.getSize(),
).toBe(2);
expect(
new NumberFieldDefinition(NumberFieldType.Int32)
new NumberFieldDefinition(NumberFieldVariant.Int32)
.create(StructDefaultOptions, struct, 42)
.getSize(),
).toBe(4);
expect(
new NumberFieldDefinition(NumberFieldType.Uint32)
new NumberFieldDefinition(NumberFieldVariant.Uint32)
.create(StructDefaultOptions, struct, 42)
.getSize(),
).toBe(4);
@ -305,7 +305,7 @@ describe("Types", () => {
describe("#serialize", () => {
it("should serialize uint8", () => {
const definition = new NumberFieldDefinition(
NumberFieldType.Int8,
NumberFieldVariant.Int8,
);
const struct = new StructValue({});
const value = definition.create(

View file

@ -15,7 +15,7 @@ import { StructFieldDefinition, StructFieldValue } from "../basic/index.js";
import { SyncPromise } from "../sync-promise.js";
import type { ValueOrPromise } from "../utils.js";
export interface NumberFieldType {
export interface NumberFieldVariant {
signed: boolean;
size: number;
deserialize(array: Uint8Array, littleEndian: boolean): number;
@ -27,8 +27,8 @@ export interface NumberFieldType {
): void;
}
export namespace NumberFieldType {
export const Uint8: NumberFieldType = {
export namespace NumberFieldVariant {
export const Uint8: NumberFieldVariant = {
signed: false,
size: 1,
deserialize(array) {
@ -39,7 +39,7 @@ export namespace NumberFieldType {
},
};
export const Int8: NumberFieldType = {
export const Int8: NumberFieldVariant = {
signed: true,
size: 1,
deserialize(array) {
@ -51,7 +51,7 @@ export namespace NumberFieldType {
},
};
export const Uint16: NumberFieldType = {
export const Uint16: NumberFieldVariant = {
signed: false,
size: 2,
deserialize(array, littleEndian) {
@ -65,7 +65,7 @@ export namespace NumberFieldType {
},
};
export const Int16: NumberFieldType = {
export const Int16: NumberFieldVariant = {
signed: true,
size: 2,
deserialize(array, littleEndian) {
@ -76,7 +76,7 @@ export namespace NumberFieldType {
},
};
export const Uint32: NumberFieldType = {
export const Uint32: NumberFieldVariant = {
signed: false,
size: 4,
deserialize(array, littleEndian) {
@ -87,7 +87,7 @@ export namespace NumberFieldType {
},
};
export const Int32: NumberFieldType = {
export const Int32: NumberFieldVariant = {
signed: true,
size: 4,
deserialize(array, littleEndian) {
@ -100,19 +100,19 @@ export namespace NumberFieldType {
}
export class NumberFieldDefinition<
TType extends NumberFieldType = NumberFieldType,
TVariant extends NumberFieldVariant = NumberFieldVariant,
TTypeScriptType = number,
> extends StructFieldDefinition<void, TTypeScriptType> {
readonly type: TType;
readonly variant: TVariant;
constructor(type: TType, typescriptType?: TTypeScriptType) {
constructor(variant: TVariant, typescriptType?: TTypeScriptType) {
void typescriptType;
super();
this.type = type;
this.variant = variant;
}
getSize(): number {
return this.type.size;
return this.variant.size;
}
create(
@ -142,7 +142,7 @@ export class NumberFieldDefinition<
return stream.readExactly(this.getSize());
})
.then((array) => {
const value = this.type.deserialize(
const value = this.variant.deserialize(
array,
options.littleEndian,
);
@ -153,10 +153,10 @@ export class NumberFieldDefinition<
}
export class NumberFieldValue<
TDefinition extends NumberFieldDefinition<NumberFieldType, unknown>,
TDefinition extends NumberFieldDefinition<NumberFieldVariant, unknown>,
> extends StructFieldValue<TDefinition> {
serialize(dataView: DataView, array: Uint8Array, offset: number): void {
this.definition.type.serialize(
this.definition.variant.serialize(
dataView,
offset,
this.value as never,

View file

@ -88,5 +88,7 @@ export function encodeUtf8(input: string): Uint8Array {
}
export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
// `TextDecoder` has internal states in stream mode,
// but we don't use stream mode here, so it's safe to reuse the instance
return Utf8Decoder.decode(buffer);
}

View file

@ -7,9 +7,9 @@
"run-eslint": "run-eslint.js"
},
"dependencies": {
"@eslint/js": "^9.1.1",
"@types/node": "^20.12.7",
"eslint": "^9.1.1",
"@eslint/js": "^9.2.0",
"@types/node": "^20.12.8",
"eslint": "^9.2.0",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0"
},