refactor: performance optimizations

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,6 +94,15 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
const buffered = new BufferedReadableStream( const buffered = new BufferedReadableStream(
stream.readable, 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); await buffered.readExactly(1);
return { return {
readable: buffered.release(), readable: buffered.release(),

View file

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

View file

@ -40,7 +40,7 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^30.0.0-alpha.3", "@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/eslint-config": "workspace:^1.0.0",
"@yume-chan/tsconfig": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View file

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

View file

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

View file

@ -35,7 +35,8 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
this.#combiner = new BufferCombiner(bufferSize); 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); await Consumable.WritableStream.write(this.#writer, buffer);
} }
@ -44,7 +45,7 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
await this.#writeLock.wait(); await this.#writeLock.wait();
const buffer = this.#combiner.flush(); const buffer = this.#combiner.flush();
if (buffer) { if (buffer) {
await this.#writeConsumable(buffer); await this.#write(buffer);
} }
} finally { } finally {
this.#writeLock.notifyOne(); this.#writeLock.notifyOne();
@ -55,7 +56,7 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
try { try {
await this.#writeLock.wait(); await this.#writeLock.wait();
for (const buffer of this.#combiner.push(data)) { for (const buffer of this.#combiner.push(data)) {
await this.#writeConsumable(buffer); await this.#write(buffer);
} }
} finally { } finally {
this.#writeLock.notifyOne(); this.#writeLock.notifyOne();
@ -63,11 +64,15 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
} }
async readExactly(length: number) { async readExactly(length: number) {
// The request may still be in the internal buffer.
// Call `flush` to send it before starting reading
await this.flush(); await this.flush();
return await this.#readable.readExactly(length); return await this.#readable.readExactly(length);
} }
release(): void { 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.#combiner.flush();
this.#socketLock.notifyOne(); this.#socketLock.notifyOne();
} }

View file

@ -24,27 +24,56 @@ import { AdbCommand, calculateChecksum } from "./packet.js";
import { AdbDaemonSocketController } from "./socket.js"; import { AdbDaemonSocketController } from "./socket.js";
export interface AdbPacketDispatcherOptions { 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. * 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; 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. maxPayloadSize: number;
* Otherwise the value must be in the range of unsigned 32-bit integer.
*/
initialDelayedAckBytes: 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; 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 { interface SocketOpenResult {
@ -225,12 +254,19 @@ export class AdbPacketDispatcher implements Closeable {
// Maybe the device is responding to a packet of last connection // Maybe the device is responding to a packet of last connection
// Tell the device to close the socket // 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) { #sendOkay(localId: number, remoteId: number, ackBytes: number) {
let payload: Uint8Array; let payload: Uint8Array;
if (this.options.initialDelayedAckBytes !== 0) { 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); payload = new Uint8Array(4);
setUint32LittleEndian(payload, 0, ackBytes); setUint32LittleEndian(payload, 0, ackBytes);
} else { } else {
@ -241,22 +277,24 @@ export class AdbPacketDispatcher implements Closeable {
} }
async #handleOpen(packet: AdbPacketData) { async #handleOpen(packet: AdbPacketData) {
// `AsyncOperationManager` doesn't support skipping IDs // Allocate a local ID for the socket from `#initializers`.
// Use `add` + `resolve` to simulate this behavior // `AsyncOperationManager` doesn't directly support returning the next ID,
// so use `add` + `resolve` to simulate this
const [localId] = this.#initializers.add<number>(); const [localId] = this.#initializers.add<number>();
this.#initializers.resolve(localId, undefined); this.#initializers.resolve(localId, undefined);
const remoteId = packet.arg0; const remoteId = packet.arg0;
let initialDelayedAckBytes = packet.arg1; let availableWriteBytes = packet.arg1;
const service = decodeUtf8(packet.payload); const service = decodeUtf8(packet.payload);
// Check remote delayed ack enablement is consistent with local
if (this.options.initialDelayedAckBytes === 0) { if (this.options.initialDelayedAckBytes === 0) {
if (initialDelayedAckBytes !== 0) { if (availableWriteBytes !== 0) {
throw new Error("Invalid OPEN packet. arg1 should be 0"); throw new Error("Invalid OPEN packet. arg1 should be 0");
} }
initialDelayedAckBytes = Infinity; availableWriteBytes = Infinity;
} else { } else {
if (initialDelayedAckBytes === 0) { if (availableWriteBytes === 0) {
throw new Error( throw new Error(
"Invalid OPEN packet. arg1 should be greater than 0", "Invalid OPEN packet. arg1 should be greater than 0",
); );
@ -265,7 +303,12 @@ export class AdbPacketDispatcher implements Closeable {
const handler = this.#incomingSocketHandlers.get(service); const handler = this.#incomingSocketHandlers.get(service);
if (!handler) { if (!handler) {
await this.sendPacket(AdbCommand.Close, 0, remoteId); await this.sendPacket(
AdbCommand.Close,
0,
remoteId,
EMPTY_UINT8_ARRAY,
);
return; return;
} }
@ -275,8 +318,8 @@ export class AdbPacketDispatcher implements Closeable {
remoteId, remoteId,
localCreated: false, localCreated: false,
service, service,
availableWriteBytes,
}); });
controller.ack(initialDelayedAckBytes);
try { try {
await handler(controller.socket); await handler(controller.socket);
@ -287,7 +330,12 @@ export class AdbPacketDispatcher implements Closeable {
this.options.initialDelayedAckBytes, this.options.initialDelayedAckBytes,
); );
} catch (e) { } 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; let handled = false;
await Promise.race([
delay(5000).then(() => { const promises: Promise<void>[] = [
if (this.options.debugSlowRead && !handled) {
throw new Error(
`packet for \`${socket.service}\` not handled in 5 seconds`,
);
}
}),
(async () => { (async () => {
await socket.enqueue(packet.payload); await socket.enqueue(packet.payload);
await this.#sendOkay( await this.#sendOkay(
@ -315,9 +357,22 @@ export class AdbPacketDispatcher implements Closeable {
); );
handled = true; 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> { async createSocket(service: string): Promise<AdbSocket> {
@ -342,8 +397,8 @@ export class AdbPacketDispatcher implements Closeable {
remoteId, remoteId,
localCreated: true, localCreated: true,
service, service,
availableWriteBytes,
}); });
controller.ack(availableWriteBytes);
this.#sockets.set(localId, controller); this.#sockets.set(localId, controller);
return controller.socket; return controller.socket;
@ -365,7 +420,8 @@ export class AdbPacketDispatcher implements Closeable {
command: AdbCommand, command: AdbCommand,
arg0: number, arg0: number,
arg1: 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> { ): Promise<void> {
if (typeof payload === "string") { if (typeof payload === "string") {
payload = encodeUtf8(payload); payload = encodeUtf8(payload);

View file

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

View file

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

View file

@ -56,26 +56,52 @@ export type AdbDaemonConnection = ReadableWritablePair<
Consumable<AdbPacketInit> Consumable<AdbPacketInit>
>; >;
interface AdbDaemonAuthenticationOptions { export interface AdbDaemonAuthenticationOptions {
serial: string; serial: string;
connection: AdbDaemonConnection; connection: AdbDaemonConnection;
credentialStore: AdbCredentialStore; credentialStore: AdbCredentialStore;
authenticators?: AdbAuthenticator[]; authenticators?: AdbAuthenticator[];
features?: readonly AdbFeature[]; features?: readonly AdbFeature[];
/** /**
* The number of bytes the device can send before receiving an ack packet. * 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. * 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. * 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; 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; 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 { interface AdbDaemonSocketConnectorConstructionOptions {
@ -85,20 +111,39 @@ interface AdbDaemonSocketConnectorConstructionOptions {
maxPayloadSize: number; maxPayloadSize: number;
banner: string; banner: string;
features?: readonly AdbFeature[]; features?: readonly AdbFeature[];
/** /**
* The number of bytes the device can send before receiving an ack packet. * 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. * When `features` doesn't include `AdbFeature.DelayedAck`, it must be set to 0. Otherwise,
* Otherwise the value must be in the range of unsigned 32-bit integer. * 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
* Delayed ack requires Android 14, this option is ignored on older versions. * 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; 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 { export class AdbDaemonTransport implements AdbTransport {
@ -106,9 +151,9 @@ export class AdbDaemonTransport implements AdbTransport {
* Authenticates the connection and creates an `AdbDaemonTransport` instance * Authenticates the connection and creates an `AdbDaemonTransport` instance
* that can be used by `Adb` class. * that can be used by `Adb` class.
* *
* If an authentication process failed, it's possible to call `authenticate` again * If an authentication process failed,
* on the same connection. Because every time the device receives a `CNXN` packet, * no matter which value the `preserveConnection` option has,
* it resets all internal state, and starts a new authentication process. * the `connection` is always kept open, so it can be used in another `authenticate` call.
*/ */
static async authenticate({ static async authenticate({
serial, serial,
@ -278,7 +323,7 @@ export class AdbDaemonTransport implements AdbTransport {
version, version,
banner, banner,
features = ADB_DAEMON_DEFAULT_FEATURES, features = ADB_DAEMON_DEFAULT_FEATURES,
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, initialDelayedAckBytes,
...options ...options
}: AdbDaemonSocketConnectorConstructionOptions) { }: AdbDaemonSocketConnectorConstructionOptions) {
this.#serial = serial; this.#serial = serial;

View file

@ -27,7 +27,12 @@ import {
import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js"; import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
import { AdbBanner } from "../banner.js"; import { AdbBanner } from "../banner.js";
import type { AdbFeature } from "../features.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"; import { AdbServerTransport } from "./transport.js";
@ -76,6 +81,23 @@ export interface AdbServerDevice {
transportId: bigint; 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 { export class AdbServerClient {
static readonly VERSION = 41; static readonly VERSION = 41;
@ -99,8 +121,27 @@ export class AdbServerClient {
return stream.readExactly(length); return stream.readExactly(length);
} }
}) })
.then((valueBuffer) => { .then((buffer) => {
return decodeUtf8(valueBuffer); // 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(); .valueOrPromise();
} }
@ -109,27 +150,29 @@ export class AdbServerClient {
writer: WritableStreamDefaultWriter<Uint8Array>, writer: WritableStreamDefaultWriter<Uint8Array>,
value: string, value: string,
): Promise<void> { ): Promise<void> {
const valueBuffer = encodeUtf8(value); // TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
const buffer = new Uint8Array(4 + valueBuffer.length); // That way allocates a new string (hopefully only a rope) instead of a new buffer
buffer.set(numberToHex(valueBuffer.length)); const encoded = encodeUtf8(value);
buffer.set(valueBuffer, 4); const buffer = new Uint8Array(4 + encoded.length);
write4HexDigits(buffer, 0, encoded.length);
buffer.set(encoded, 4);
await writer.write(buffer); await writer.write(buffer);
} }
static async readOkay( static async readOkay(
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
): Promise<void> { ): Promise<void> {
const response = decodeUtf8(await stream.readExactly(4)); const response = await stream.readExactly(4);
if (response === "OKAY") { if (sequenceEqual(response, OKAY)) {
return; return;
} }
if (response === "FAIL") { if (sequenceEqual(response, FAIL)) {
const reason = await AdbServerClient.readString(stream); const reason = await AdbServerClient.readString(stream);
throw new Error(reason); throw new Error(reason);
} }
throw new Error(`Unexpected response: ${response}`); throw new Error(`Unexpected response: ${decodeUtf8(response)}`);
} }
async connect( async connect(

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^30.0.0-alpha.3", "@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/eslint-config": "workspace:^1.0.0",
"@yume-chan/tsconfig": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View file

@ -344,8 +344,11 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
return; 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 // 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; let data: Uint8Array;
if (this.#config !== undefined) { if (this.#config !== undefined) {
data = new Uint8Array( data = new Uint8Array(

View file

@ -91,145 +91,6 @@ export function* annexBSplitNalu(buffer: Uint8Array): Generator<Uint8Array> {
yield buffer.subarray(start, buffer.length); 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 { export class NaluSodbBitReader {
readonly #nalu: Uint8Array; readonly #nalu: Uint8Array;
// logical length is `#byteLength * 8 + (7 - #stopBitIndex)` // logical length is `#byteLength * 8 + (7 - #stopBitIndex)`
@ -269,6 +130,7 @@ export class NaluSodbBitReader {
constructor(nalu: Uint8Array) { constructor(nalu: Uint8Array) {
this.#nalu = nalu; 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) { for (let i = nalu.length - 1; i >= 0; i -= 1) {
if (this.#nalu[i] === 0) { if (this.#nalu[i] === 0) {
continue; continue;
@ -292,16 +154,18 @@ export class NaluSodbBitReader {
this.#byte = this.#nalu[this.#bytePosition]!; this.#byte = this.#nalu[this.#bytePosition]!;
// If the current sequence is `0x000003`, skip to the next byte. // 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) { if (this.#zeroCount === 2 && this.#byte === 3) {
this.#zeroCount = 0; this.#zeroCount = 0;
this.#bytePosition += 1; this.#bytePosition += 1;
// Call `#loadByte` again, because if the next byte is `0x00`,
// it need to be counted in `#zeroCount` as well.
this.#loadByte(); this.#loadByte();
return; return;
} }
// `0x00000301` becomes `0x000001`, so only the `0x03` byte needs to be skipped // `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) { if (this.#byte === 0) {
this.#zeroCount += 1; this.#zeroCount += 1;
} else { } else {
@ -338,7 +202,13 @@ export class NaluSodbBitReader {
return result; 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 ( if (
this.#bytePosition >= this.#byteLength && this.#bytePosition >= this.#byteLength &&
this.#bitPosition < this.#stopBitIndex this.#bitPosition < this.#stopBitIndex
@ -350,24 +220,29 @@ export class NaluSodbBitReader {
skip(length: number) { skip(length: number) {
if (length <= this.#bitPosition + 1) { if (length <= this.#bitPosition + 1) {
this.#bitPosition -= length; this.#bitPosition -= length;
this.#ensurePositionValid(); this.#checkSkipPosition();
return; 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; length -= this.#bitPosition + 1;
this.#bytePosition += 1; this.#bytePosition += 1;
this.#bitPosition = 7; this.#bitPosition = 7;
this.#loadByte(); this.#loadByte();
this.#ensurePositionValid(); this.#checkSkipPosition();
for (; length >= 8; length -= 8) { for (; length >= 8; length -= 8) {
this.#bytePosition += 1; this.#bytePosition += 1;
this.#loadByte(); this.#loadByte();
this.#ensurePositionValid(); this.#checkSkipPosition();
} }
this.#bitPosition = 7 - length; this.#bitPosition = 7 - length;
this.#ensurePositionValid(); this.#checkSkipPosition();
} }
decodeExponentialGolombNumber(): number { decodeExponentialGolombNumber(): number {

View file

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

View file

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

View file

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

View file

@ -83,13 +83,20 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
return order.map((key) => toScrcpyOptionValue(options[key], "-")); 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( static async parseCString(
stream: AsyncExactReadable, stream: AsyncExactReadable,
maxLength: number, maxLength: number,
): Promise<string> { ): Promise<string> {
let result = decodeUtf8(await stream.readExactly(maxLength)); const buffer = await stream.readExactly(maxLength);
result = result.substring(0, result.indexOf("\0")); // If null terminator is not found, `subarray(0, -1)` will remove the last byte
return result; // 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> { static async parseUint16BE(stream: AsyncExactReadable): Promise<number> {

View file

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

View file

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

View file

@ -14,8 +14,8 @@ import type {
import { import {
CodecOptions, CodecOptions,
ScrcpyFloatToUint16FieldDefinition,
ScrcpyOptions1_16, ScrcpyOptions1_16,
ScrcpyUnsignedFloatFieldDefinition,
} from "./1_16/index.js"; } from "./1_16/index.js";
import { ScrcpyOptions1_21 } from "./1_21.js"; import { ScrcpyOptions1_21 } from "./1_21.js";
import type { ScrcpyOptionsInit1_24 } from "./1_24.js"; import type { ScrcpyOptionsInit1_24 } from "./1_24.js";
@ -38,7 +38,7 @@ export const ScrcpyInjectTouchControlMessage2_0 = new Struct()
.uint32("pointerY") .uint32("pointerY")
.uint16("screenWidth") .uint16("screenWidth")
.uint16("screenHeight") .uint16("screenHeight")
.field("pressure", ScrcpyFloatToUint16FieldDefinition) .field("pressure", ScrcpyUnsignedFloatFieldDefinition)
.uint32("actionButton") .uint32("actionButton")
.uint32("buttons"); .uint32("buttons");
@ -107,6 +107,58 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
ScrcpyOptionsInit2_0, ScrcpyOptionsInit2_0,
ScrcpyOptions1_25 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 = { static readonly DEFAULTS = {
...omit(ScrcpyOptions1_24.DEFAULTS, [ ...omit(ScrcpyOptions1_24.DEFAULTS, [
"bitRate", "bitRate",
@ -255,81 +307,34 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
override parseAudioStreamMetadata( override parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> { ): ValueOrPromise<ScrcpyAudioStreamMetadata> {
return (async (): Promise<ScrcpyAudioStreamMetadata> => { return ScrcpyOptions2_0.parseAudioMetadata(
const buffered = new BufferedReadableStream(stream); stream,
const buffer = await buffered.readExactly(4); this.value.sendCodecMeta,
(value) => {
const codecMetadataValue = getUint32BigEndian(buffer, 0); switch (value) {
// 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;
case ScrcpyAudioCodec.RAW.metadataValue: case ScrcpyAudioCodec.RAW.metadataValue:
codec = ScrcpyAudioCodec.RAW; return ScrcpyAudioCodec.RAW;
break; case ScrcpyAudioCodec.OPUS.metadataValue:
return ScrcpyAudioCodec.OPUS;
case ScrcpyAudioCodec.AAC.metadataValue:
return ScrcpyAudioCodec.AAC;
default: default:
throw new Error( throw new Error(
`Unknown audio codec metadata value: ${codecMetadataValue}`, `Unknown audio codec metadata value: ${value}`,
); );
} }
return { },
type: "success", () => {
codec, switch (this.value.audioCodec) {
stream: buffered.release(), case "raw":
}; return ScrcpyAudioCodec.RAW;
} case "opus":
return ScrcpyAudioCodec.OPUS;
// Infer codec from `audioCodec` option case "aac":
let codec: ScrcpyAudioCodec; return ScrcpyAudioCodec.AAC;
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);
}
},
),
};
})();
} }
override serializeInjectTouchControlMessage( override serializeInjectTouchControlMessage(

View file

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

View file

@ -1,4 +1,5 @@
import { PromiseResolver } from "@yume-chan/async"; import { PromiseResolver } from "@yume-chan/async";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import type { ReadableStreamDefaultController } from "./stream.js"; import type { ReadableStreamDefaultController } from "./stream.js";
import { ReadableStream, WritableStream } 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, * If you want to decode the result as string,
* prefer `.pipeThrough(new DecodeUtf8Stream()).pipeThrough(new ConcatStringStream())`, * prefer `.pipeThrough(new DecodeUtf8Stream()).pipeThrough(new ConcatStringStream())`,
* than `.pipeThough(new ConcatBufferStream()).pipeThrough(new DecodeUtf8Stream())`, * 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 { export class ConcatBufferStream {
#segments: Uint8Array[] = []; #segments: Uint8Array[] = [];
@ -99,7 +101,7 @@ export class ConcatBufferStream {
let offset = 0; let offset = 0;
switch (this.#segments.length) { switch (this.#segments.length) {
case 0: case 0:
result = new Uint8Array(0); result = EMPTY_UINT8_ARRAY;
break; break;
case 1: case 1:
result = this.#segments[0]!; result = this.#segments[0]!;

View file

@ -7,30 +7,9 @@ import type {
} from "./stream.js"; } from "./stream.js";
import { import {
WritableStream as NativeWritableStream, WritableStream as NativeWritableStream,
ReadableStream, ReadableStream as NativeReadableStream,
} from "./stream.js"; } from "./stream.js";
import { createTask, type Task } from "./task.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();
},
}));
function isPromiseLike(value: unknown): value is PromiseLike<unknown> { function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return typeof value === "object" && value !== null && "then" in value; return typeof value === "object" && value !== null && "then" in value;
@ -148,83 +127,78 @@ export namespace Consumable {
); );
} }
} }
}
export interface ConsumableReadableStreamController<T> { export interface ReadableStreamController<T> {
enqueue(chunk: T): Promise<void>; enqueue(chunk: T): Promise<void>;
close(): void; close(): void;
error(reason: unknown): 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;
} }
constructor( export interface ReadableStreamSource<T> {
source: ConsumableReadableStreamSource<T>, start?(
strategy?: QueuingStrategy<T>, controller: ReadableStreamController<T>,
) { ): void | PromiseLike<void>;
let wrappedController: pull?(
| ConsumableReadableStreamController<T> controller: ReadableStreamController<T>,
| undefined; ): void | PromiseLike<void>;
cancel?(reason: unknown): void | PromiseLike<void>;
}
let wrappedStrategy: QueuingStrategy<Consumable<T>> | undefined; export class ReadableStream<T> extends NativeReadableStream<Consumable<T>> {
if (strategy) { static async enqueue<T>(
wrappedStrategy = {}; controller: { enqueue: (chunk: Consumable<T>) => void },
if ("highWaterMark" in strategy) { chunk: T,
wrappedStrategy.highWaterMark = strategy.highWaterMark; ) {
} const output = new Consumable(chunk);
if ("size" in strategy) { controller.enqueue(output);
wrappedStrategy.size = (chunk) => { await output.consumed;
return strategy.size!(chunk.value);
};
}
} }
super( constructor(
{ source: ReadableStreamSource<T>,
async start(controller) { strategy?: QueuingStrategy<T>,
wrappedController = { ) {
async enqueue(chunk) { let wrappedController: ReadableStreamController<T> | undefined;
await ConsumableReadableStream.enqueue(
controller,
chunk,
);
},
close() {
controller.close();
},
error(reason) {
controller.error(reason);
},
};
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() { wrappedStrategy,
await source.pull?.(wrappedController!); );
}, }
async cancel(reason) {
await source.cancel?.(reason);
},
},
wrappedStrategy,
);
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import type {
import { StructFieldValue } from "../../basic/index.js"; import { StructFieldValue } from "../../basic/index.js";
import type { KeysOfType } from "../../utils.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"; import { BufferLikeFieldDefinition, BufferLikeFieldValue } from "./base.js";
export type LengthField<TFields> = KeysOfType<TFields, number | string>; export type LengthField<TFields> = KeysOfType<TFields, number | string>;
@ -35,12 +35,12 @@ export interface VariableLengthBufferLikeFieldOptions<
} }
export class VariableLengthBufferLikeFieldDefinition< export class VariableLengthBufferLikeFieldDefinition<
TType extends BufferFieldSubType = BufferFieldSubType, TConverter extends BufferFieldConverter = BufferFieldConverter,
TOptions extends TOptions extends
VariableLengthBufferLikeFieldOptions = VariableLengthBufferLikeFieldOptions, VariableLengthBufferLikeFieldOptions = VariableLengthBufferLikeFieldOptions,
TTypeScriptType = TType["TTypeScriptType"], TTypeScriptType = TConverter["TTypeScriptType"],
> extends BufferLikeFieldDefinition< > extends BufferLikeFieldDefinition<
TType, TConverter,
TOptions, TOptions,
TOptions["lengthField"], TOptions["lengthField"],
TTypeScriptType TTypeScriptType
@ -106,14 +106,24 @@ export class VariableLengthBufferLikeStructFieldValue<
} }
override getSize() { override getSize() {
if (this.length === undefined) { if (this.length !== undefined) {
this.length = this.definition.type.getSize(this.value); // Have cached length
if (this.length === -1) { return this.length;
this.array = this.definition.type.toBuffer(this.value);
this.length = this.array.byteLength;
}
} }
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; return this.length;
} }

View file

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

View file

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

View file

@ -88,5 +88,7 @@ export function encodeUtf8(input: string): Uint8Array {
} }
export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string { 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); return Utf8Decoder.decode(buffer);
} }

View file

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