mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
refactor: performance optimizations
also renamed some internal types, and adding more code comments. (not thoroughly tested yet)
This commit is contained in:
parent
65b8671b66
commit
8223d79886
49 changed files with 932 additions and 767 deletions
|
@ -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",
|
||||
|
|
166
common/config/rush/pnpm-lock.yaml
generated
166
common/config/rush/pnpm-lock.yaml
generated
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,11 +47,21 @@ 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) {
|
||||
await AdbReverseErrorResponse.deserialize(stream);
|
||||
|
||||
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 &&
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await this.#writer.write(
|
||||
AdbShellProtocolPacket.serialize({
|
||||
id: AdbShellProtocolId.Stdin,
|
||||
data: chunk,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,6 @@
|
|||
"gh-release-fetch": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.7"
|
||||
"@types/node": "^20.12.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
);
|
|
@ -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";
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
};
|
||||
})();
|
||||
},
|
||||
() => {
|
||||
switch (this.value.audioCodec) {
|
||||
case "raw":
|
||||
return ScrcpyAudioCodec.RAW;
|
||||
case "opus":
|
||||
return ScrcpyAudioCodec.OPUS;
|
||||
case "aac":
|
||||
return ScrcpyAudioCodec.AAC;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
override serializeInjectTouchControlMessage(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]!;
|
||||
|
|
|
@ -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,83 +127,78 @@ export namespace Consumable {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConsumableReadableStreamController<T> {
|
||||
enqueue(chunk: T): Promise<void>;
|
||||
close(): void;
|
||||
error(reason: unknown): void;
|
||||
}
|
||||
|
||||
export interface ConsumableReadableStreamSource<T> {
|
||||
start?(
|
||||
controller: ConsumableReadableStreamController<T>,
|
||||
): void | PromiseLike<void>;
|
||||
pull?(
|
||||
controller: ConsumableReadableStreamController<T>,
|
||||
): void | PromiseLike<void>;
|
||||
cancel?(reason: unknown): void | PromiseLike<void>;
|
||||
}
|
||||
|
||||
export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
|
||||
static async enqueue<T>(
|
||||
controller: { enqueue: (chunk: Consumable<T>) => void },
|
||||
chunk: T,
|
||||
) {
|
||||
const output = new Consumable(chunk);
|
||||
controller.enqueue(output);
|
||||
await output.consumed;
|
||||
export interface ReadableStreamController<T> {
|
||||
enqueue(chunk: T): Promise<void>;
|
||||
close(): void;
|
||||
error(reason: unknown): void;
|
||||
}
|
||||
|
||||
constructor(
|
||||
source: ConsumableReadableStreamSource<T>,
|
||||
strategy?: QueuingStrategy<T>,
|
||||
) {
|
||||
let wrappedController:
|
||||
| ConsumableReadableStreamController<T>
|
||||
| undefined;
|
||||
export interface ReadableStreamSource<T> {
|
||||
start?(
|
||||
controller: ReadableStreamController<T>,
|
||||
): void | PromiseLike<void>;
|
||||
pull?(
|
||||
controller: ReadableStreamController<T>,
|
||||
): void | PromiseLike<void>;
|
||||
cancel?(reason: unknown): void | PromiseLike<void>;
|
||||
}
|
||||
|
||||
let wrappedStrategy: QueuingStrategy<Consumable<T>> | undefined;
|
||||
if (strategy) {
|
||||
wrappedStrategy = {};
|
||||
if ("highWaterMark" in strategy) {
|
||||
wrappedStrategy.highWaterMark = strategy.highWaterMark;
|
||||
}
|
||||
if ("size" in strategy) {
|
||||
wrappedStrategy.size = (chunk) => {
|
||||
return strategy.size!(chunk.value);
|
||||
};
|
||||
}
|
||||
export class ReadableStream<T> extends NativeReadableStream<Consumable<T>> {
|
||||
static async enqueue<T>(
|
||||
controller: { enqueue: (chunk: Consumable<T>) => void },
|
||||
chunk: T,
|
||||
) {
|
||||
const output = new Consumable(chunk);
|
||||
controller.enqueue(output);
|
||||
await output.consumed;
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
async start(controller) {
|
||||
wrappedController = {
|
||||
async enqueue(chunk) {
|
||||
await ConsumableReadableStream.enqueue(
|
||||
controller,
|
||||
chunk,
|
||||
);
|
||||
},
|
||||
close() {
|
||||
controller.close();
|
||||
},
|
||||
error(reason) {
|
||||
controller.error(reason);
|
||||
},
|
||||
};
|
||||
constructor(
|
||||
source: ReadableStreamSource<T>,
|
||||
strategy?: QueuingStrategy<T>,
|
||||
) {
|
||||
let wrappedController: ReadableStreamController<T> | undefined;
|
||||
|
||||
await source.start?.(wrappedController);
|
||||
let wrappedStrategy: QueuingStrategy<Consumable<T>> | undefined;
|
||||
if (strategy) {
|
||||
wrappedStrategy = {};
|
||||
if ("highWaterMark" in strategy) {
|
||||
wrappedStrategy.highWaterMark = strategy.highWaterMark;
|
||||
}
|
||||
if ("size" in strategy) {
|
||||
wrappedStrategy.size = (chunk) => {
|
||||
return strategy.size!(chunk.value);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
super(
|
||||
{
|
||||
async start(controller) {
|
||||
wrappedController = {
|
||||
async enqueue(chunk) {
|
||||
await ReadableStream.enqueue(controller, chunk);
|
||||
},
|
||||
close() {
|
||||
controller.close();
|
||||
},
|
||||
error(reason) {
|
||||
controller.error(reason);
|
||||
},
|
||||
};
|
||||
|
||||
await source.start?.(wrappedController);
|
||||
},
|
||||
async pull() {
|
||||
await source.pull?.(wrappedController!);
|
||||
},
|
||||
async cancel(reason) {
|
||||
await source.cancel?.(reason);
|
||||
},
|
||||
},
|
||||
async pull() {
|
||||
await source.pull?.(wrappedController!);
|
||||
},
|
||||
async cancel(reason) {
|
||||
await source.cancel?.(reason);
|
||||
},
|
||||
},
|
||||
wrappedStrategy,
|
||||
);
|
||||
wrappedStrategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
|
21
libraries/stream-extra/src/task.ts
Normal file
21
libraries/stream-extra/src/task.ts
Normal 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();
|
||||
},
|
||||
}));
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
void value;
|
||||
return this.size;
|
||||
});
|
||||
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 },
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue