diff --git a/apps/cli/package.json b/apps/cli/package.json index 5ba27a78..8dada264 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,7 +43,7 @@ "tslib": "^2.6.2" }, "devDependencies": { - "@types/node": "^20.12.7", + "@types/node": "^20.12.8", "@yume-chan/eslint-config": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0", "jest": "^30.0.0-alpha.3", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index c6bbe7c0..fea7e987 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: version: 2.6.2 devDependencies: '@types/node': - specifier: ^20.12.7 - version: 20.12.7 + specifier: ^20.12.8 + version: 20.12.8 '@yume-chan/eslint-config': specifier: workspace:^1.0.0 version: link:../../toolchain/eslint-config @@ -43,7 +43,7 @@ importers: version: link:../../toolchain/tsconfig jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -73,8 +73,8 @@ importers: specifier: ^30.0.0-alpha.3 version: 30.0.0-alpha.3 '@types/node': - specifier: ^20.12.7 - version: 20.12.7 + specifier: ^20.12.8 + version: 20.12.8 '@yume-chan/eslint-config': specifier: workspace:^1.0.0 version: link:../../toolchain/eslint-config @@ -86,7 +86,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -179,7 +179,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -203,8 +203,8 @@ importers: version: link:../struct devDependencies: '@types/node': - specifier: ^20.12.7 - version: 20.12.7 + specifier: ^20.12.8 + version: 20.12.8 '@yume-chan/eslint-config': specifier: workspace:^1.0.0 version: link:../../toolchain/eslint-config @@ -213,7 +213,7 @@ importers: version: link:../../toolchain/tsconfig jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -247,7 +247,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -294,7 +294,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -312,8 +312,8 @@ importers: version: 4.0.3 devDependencies: '@types/node': - specifier: ^20.12.7 - version: 20.12.7 + specifier: ^20.12.8 + version: 20.12.8 ../../libraries/no-data-view: devDependencies: @@ -321,8 +321,8 @@ importers: specifier: ^30.0.0-alpha.3 version: 30.0.0-alpha.3 '@types/node': - specifier: ^20.12.7 - version: 20.12.7 + specifier: ^20.12.8 + version: 20.12.8 '@yume-chan/eslint-config': specifier: workspace:^1.0.0 version: link:../../toolchain/eslint-config @@ -334,7 +334,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -367,7 +367,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -410,7 +410,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -459,7 +459,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -502,7 +502,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -536,7 +536,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -567,7 +567,7 @@ importers: version: 7.0.3 jest: specifier: ^30.0.0-alpha.3 - version: 30.0.0-alpha.3(@types/node@20.12.7) + version: 30.0.0-alpha.3(@types/node@20.12.8) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -581,20 +581,20 @@ importers: ../../toolchain/eslint-config: dependencies: '@eslint/js': - specifier: ^9.1.1 - version: 9.1.1 + specifier: ^9.2.0 + version: 9.2.0 '@types/node': - specifier: ^20.12.7 - version: 20.12.7 + specifier: ^20.12.8 + version: 20.12.8 eslint: - specifier: ^9.1.1 - version: 9.1.1 + specifier: ^9.2.0 + version: 9.2.0 typescript: specifier: ^5.4.5 version: 5.4.5 typescript-eslint: specifier: ^7.8.0 - version: 7.8.0(eslint@9.1.1)(typescript@5.4.5) + version: 7.8.0(eslint@9.2.0)(typescript@5.4.5) devDependencies: prettier: specifier: ^3.2.5 @@ -944,13 +944,13 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@9.1.1): + /@eslint-community/eslint-utils@4.4.0(eslint@9.2.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 9.1.1 + eslint: 9.2.0 eslint-visitor-keys: 3.4.3 dev: false @@ -976,8 +976,8 @@ packages: - supports-color dev: false - /@eslint/js@9.1.1: - resolution: {integrity: sha512-5WoDz3Y19Bg2BnErkZTp0en+c/i9PvgFS7MBe1+m60HjFr0hrphlAGp4yzI7pxpt4xShln4ZyYp4neJm8hmOkQ==} + /@eslint/js@9.2.0: + resolution: {integrity: sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: false @@ -1039,7 +1039,7 @@ packages: engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} dependencies: '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 jest-message-util: 30.0.0-alpha.3 jest-util: 30.0.0-alpha.3 @@ -1060,14 +1060,14 @@ packages: '@jest/test-result': 30.0.0-alpha.3 '@jest/transform': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.0.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 30.0.0-alpha.3 - jest-config: 30.0.0-alpha.3(@types/node@20.12.7) + jest-config: 30.0.0-alpha.3(@types/node@20.12.8) jest-haste-map: 30.0.0-alpha.3 jest-message-util: 30.0.0-alpha.3 jest-regex-util: 30.0.0-alpha.3 @@ -1095,7 +1095,7 @@ packages: dependencies: '@jest/fake-timers': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 jest-mock: 30.0.0-alpha.3 dev: true @@ -1122,7 +1122,7 @@ packages: dependencies: '@jest/types': 30.0.0-alpha.3 '@sinonjs/fake-timers': 11.2.2 - '@types/node': 20.12.7 + '@types/node': 20.12.8 jest-message-util: 30.0.0-alpha.3 jest-mock: 30.0.0-alpha.3 jest-util: 30.0.0-alpha.3 @@ -1155,7 +1155,7 @@ packages: '@jest/transform': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -1250,7 +1250,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.12.7 + '@types/node': 20.12.8 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -1262,7 +1262,7 @@ packages: '@jest/schemas': 30.0.0-alpha.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.12.7 + '@types/node': 20.12.8 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -1423,8 +1423,8 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: false - /@types/node@20.12.7: - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + /@types/node@20.12.8: + resolution: {integrity: sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==} dependencies: undici-types: 5.26.5 @@ -1450,7 +1450,7 @@ packages: '@types/yargs-parser': 21.0.3 dev: true - /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.1.1)(typescript@5.4.5): + /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.2.0)(typescript@5.4.5): resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -1462,13 +1462,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.8.0(eslint@9.1.1)(typescript@5.4.5) + '@typescript-eslint/parser': 7.8.0(eslint@9.2.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/type-utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5) + '@typescript-eslint/type-utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 - eslint: 9.1.1 + eslint: 9.2.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 @@ -1479,7 +1479,7 @@ packages: - supports-color dev: false - /@typescript-eslint/parser@7.8.0(eslint@9.1.1)(typescript@5.4.5): + /@typescript-eslint/parser@7.8.0(eslint@9.2.0)(typescript@5.4.5): resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -1494,7 +1494,7 @@ packages: '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 - eslint: 9.1.1 + eslint: 9.2.0 typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -1508,7 +1508,7 @@ packages: '@typescript-eslint/visitor-keys': 7.8.0 dev: false - /@typescript-eslint/type-utils@7.8.0(eslint@9.1.1)(typescript@5.4.5): + /@typescript-eslint/type-utils@7.8.0(eslint@9.2.0)(typescript@5.4.5): resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -1519,9 +1519,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5) debug: 4.3.4 - eslint: 9.1.1 + eslint: 9.2.0 ts-api-utils: 1.3.0(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: @@ -1555,19 +1555,19 @@ packages: - supports-color dev: false - /@typescript-eslint/utils@7.8.0(eslint@9.1.1)(typescript@5.4.5): + /@typescript-eslint/utils@7.8.0(eslint@9.2.0)(typescript@5.4.5): resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.1.1) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.2.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 7.8.0 '@typescript-eslint/types': 7.8.0 '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - eslint: 9.1.1 + eslint: 9.2.0 semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -2171,15 +2171,15 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: false - /eslint@9.1.1: - resolution: {integrity: sha512-b4cRQ0BeZcSEzPpY2PjFY70VbO32K7BStTGtBsnIGdTSEEQzBi8hPBcGQmTG2zUvFr9uLe0TK42bw8YszuHEqg==} + /eslint@9.2.0: + resolution: {integrity: sha512-0n/I88vZpCOzO+PQpt0lbsqmn9AsnsJAQseIqhZFI8ibQT0U1AkEKRxA3EVMos0BoHSXDQvCXY25TUjB5tr8Og==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.1.1) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.2.0) '@eslint-community/regexpp': 4.10.0 '@eslint/eslintrc': 3.0.2 - '@eslint/js': 9.1.1 + '@eslint/js': 9.2.0 '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.2.3 @@ -2794,7 +2794,7 @@ packages: '@jest/expect': 30.0.0-alpha.3 '@jest/test-result': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -2815,7 +2815,7 @@ packages: - supports-color dev: true - /jest-cli@30.0.0-alpha.3(@types/node@20.12.7): + /jest-cli@30.0.0-alpha.3(@types/node@20.12.8): resolution: {integrity: sha512-z1aQDxDe0VeDSEUeMr9MrfI5cc2SSCiKtG0Rt3XDfTgWrzyoakVds/9QMkkpNKHryCBzZZKOMe5W2uy7qM4WOA==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} hasBin: true @@ -2831,7 +2831,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 import-local: 3.1.0 - jest-config: 30.0.0-alpha.3(@types/node@20.12.7) + jest-config: 30.0.0-alpha.3(@types/node@20.12.8) jest-util: 30.0.0-alpha.3 jest-validate: 30.0.0-alpha.3 yargs: 17.7.2 @@ -2842,7 +2842,7 @@ packages: - ts-node dev: true - /jest-config@30.0.0-alpha.3(@types/node@20.12.7): + /jest-config@30.0.0-alpha.3(@types/node@20.12.8): resolution: {integrity: sha512-3eqS6gcsaPtcpU/VVlkLx1se1JiH18uh1Xg+oOf6FhlLDvAT5h6+dvWa2IpyucCN46dHHEw3E85qfjogq4XLtw==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} peerDependencies: @@ -2857,7 +2857,7 @@ packages: '@babel/core': 7.24.5 '@jest/test-sequencer': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 babel-jest: 30.0.0-alpha.3(@babel/core@7.24.5) chalk: 4.1.2 ci-info: 4.0.0 @@ -2917,7 +2917,7 @@ packages: '@jest/environment': 30.0.0-alpha.3 '@jest/fake-timers': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 jest-mock: 30.0.0-alpha.3 jest-util: 30.0.0-alpha.3 dev: true @@ -2932,7 +2932,7 @@ packages: engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} dependencies: '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2983,7 +2983,7 @@ packages: engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} dependencies: '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 jest-util: 30.0.0-alpha.3 dev: true @@ -3038,7 +3038,7 @@ packages: '@jest/test-result': 30.0.0-alpha.3 '@jest/transform': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -3069,7 +3069,7 @@ packages: '@jest/test-result': 30.0.0-alpha.3 '@jest/transform': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 @@ -3122,7 +3122,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3134,7 +3134,7 @@ packages: engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} dependencies: '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 chalk: 4.1.2 ci-info: 4.0.0 graceful-fs: 4.2.11 @@ -3159,7 +3159,7 @@ packages: dependencies: '@jest/test-result': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 - '@types/node': 20.12.7 + '@types/node': 20.12.8 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3171,13 +3171,13 @@ packages: resolution: {integrity: sha512-8lS9LxbEjOyBRz0Pdi6m3HYJ3feIi1tv0u7oqxjXvB1lMksq+IcSxaPTCcvJbIqt3WAFFYQnDs5I3NkJiEG5Ow==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} dependencies: - '@types/node': 20.12.7 + '@types/node': 20.12.8 jest-util: 30.0.0-alpha.3 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@30.0.0-alpha.3(@types/node@20.12.7): + /jest@30.0.0-alpha.3(@types/node@20.12.8): resolution: {integrity: sha512-oJndFRnG1Xsc1ybac44hGGj7+O4nT9losg8+8YDjNwDAXbYwvzyRgmCiPo6L/BROiAD8Z9qGgFRsFuGdpmQuFw==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} hasBin: true @@ -3190,7 +3190,7 @@ packages: '@jest/core': 30.0.0-alpha.3 '@jest/types': 30.0.0-alpha.3 import-local: 3.1.0 - jest-cli: 30.0.0-alpha.3(@types/node@20.12.7) + jest-cli: 30.0.0-alpha.3(@types/node@20.12.8) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -4017,7 +4017,7 @@ packages: dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 30.0.0-alpha.3(@types/node@20.12.7) + jest: 30.0.0-alpha.3(@types/node@20.12.8) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -4047,7 +4047,7 @@ packages: engines: {node: '>=10'} dev: true - /typescript-eslint@7.8.0(eslint@9.1.1)(typescript@5.4.5): + /typescript-eslint@7.8.0(eslint@9.2.0)(typescript@5.4.5): resolution: {integrity: sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -4057,10 +4057,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.1.1)(typescript@5.4.5) - '@typescript-eslint/parser': 7.8.0(eslint@9.1.1)(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@9.1.1)(typescript@5.4.5) - eslint: 9.1.1 + '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@9.2.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.8.0(eslint@9.2.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@9.2.0)(typescript@5.4.5) + eslint: 9.2.0 typescript: 5.4.5 transitivePeerDependencies: - supports-color diff --git a/common/config/rush/repo-state.json b/common/config/rush/repo-state.json index 70aa9dd8..6f0634e8 100644 --- a/common/config/rush/repo-state.json +++ b/common/config/rush/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "c1a7aa8a614d364cced2ff0b11ae2cd00e133961", + "pnpmShrinkwrapHash": "ca7743cb8480b533789651d98878a7900c9455fa", "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" } diff --git a/libraries/adb-scrcpy/src/client.ts b/libraries/adb-scrcpy/src/client.ts index d6e4799a..c416abf9 100644 --- a/libraries/adb-scrcpy/src/client.ts +++ b/libraries/adb-scrcpy/src/client.ts @@ -333,10 +333,10 @@ export class AdbScrcpyClient { async #parseDeviceMessages(controlStream: ReadableStream) { const buffered = new BufferedReadableStream(controlStream); while (true) { - const type = await buffered.readExactly(1); - if (!(await this.#options.parseDeviceMessage(type[0]!, buffered))) { + const [type] = await buffered.readExactly(1); + if (!(await this.#options.parseDeviceMessage(type!, buffered))) { buffered - .cancel(new Error(`Unknown device message type ${type[0]}`)) + .cancel(new Error(`Unknown device message type ${type!}`)) .catch(() => {}); break; } diff --git a/libraries/adb-scrcpy/src/connection.ts b/libraries/adb-scrcpy/src/connection.ts index 4cd9b4f6..d272c06f 100644 --- a/libraries/adb-scrcpy/src/connection.ts +++ b/libraries/adb-scrcpy/src/connection.ts @@ -94,6 +94,15 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection { const buffered = new BufferedReadableStream( stream.readable, ); + // Skip the dummy byte + // Google ADB forward tunnel listens on a socket on the computer, + // when a client connects to that socket, Google ADB will forward + // the connection to the socket on the device. + // However, connecting to that socket will always succeed immediately, + // which doesn't mean that Google ADB has connected to + // the socket on the device. + // Thus Scrcpy server sends a dummy byte to the socket, to let the client + // know that the connection is truly established. await buffered.readExactly(1); return { readable: buffered.release(), diff --git a/libraries/adb-server-node-tcp/package.json b/libraries/adb-server-node-tcp/package.json index eb337210..9de920e0 100644 --- a/libraries/adb-server-node-tcp/package.json +++ b/libraries/adb-server-node-tcp/package.json @@ -37,7 +37,7 @@ "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { - "@types/node": "^20.12.7", + "@types/node": "^20.12.8", "@yume-chan/eslint-config": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0", "jest": "^30.0.0-alpha.3", diff --git a/libraries/adb/package.json b/libraries/adb/package.json index 2addd185..9eda18f9 100644 --- a/libraries/adb/package.json +++ b/libraries/adb/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", - "@types/node": "^20.12.7", + "@types/node": "^20.12.8", "@yume-chan/eslint-config": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0", "cross-env": "^7.0.3", diff --git a/libraries/adb/src/commands/reverse.ts b/libraries/adb/src/commands/reverse.ts index 3572ca76..da3dade8 100644 --- a/libraries/adb/src/commands/reverse.ts +++ b/libraries/adb/src/commands/reverse.ts @@ -2,10 +2,10 @@ import { AutoDisposable } from "@yume-chan/event"; import { BufferedReadableStream } from "@yume-chan/stream-extra"; -import Struct, { ExactReadableEndedError } from "@yume-chan/struct"; +import Struct, { ExactReadableEndedError, encodeUtf8 } from "@yume-chan/struct"; import type { Adb, AdbIncomingSocketHandler } from "../adb.js"; -import { decodeUtf8, hexToNumber } from "../utils/index.js"; +import { hexToNumber } from "../utils/index.js"; export interface AdbForwardListener { deviceSerial: string; @@ -47,11 +47,21 @@ const AdbReverseErrorResponse = new Struct() } }); -async function readString(stream: BufferedReadableStream, length: number) { - const buffer = await stream.readExactly(length); - return decodeUtf8(buffer); +// Like `hexToNumber`, it's much faster than first converting `buffer` to a string +function decimalToNumber(buffer: Uint8Array) { + let value = 0; + for (const byte of buffer) { + // Like `parseInt`, return when it encounters a non-digit character + if (byte < 48 || byte > 57) { + return value; + } + value = value * 10 + byte - 48; + } + return value; } +const OKAY = encodeUtf8("OKAY"); + export class AdbReverseCommand extends AutoDisposable { protected adb: Adb; @@ -70,10 +80,14 @@ export class AdbReverseCommand extends AutoDisposable { protected async sendRequest(service: string) { const stream = await this.createBufferedStream(service); - const success = (await readString(stream, 4)) === "OKAY"; - if (!success) { - await AdbReverseErrorResponse.deserialize(stream); + + const response = await stream.readExactly(4); + for (let i = 0; i < 4; i += 1) { + if (response[i] !== OKAY[i]) { + await AdbReverseErrorResponse.deserialize(stream); + } } + return stream; } @@ -110,8 +124,8 @@ export class AdbReverseCommand extends AutoDisposable { const position = stream.position; try { const length = hexToNumber(await stream.readExactly(4)); - const port = await readString(stream, length); - deviceAddress = `tcp:${Number.parseInt(port, 10)}`; + const port = decimalToNumber(await stream.readExactly(length)); + deviceAddress = `tcp:${port}`; } catch (e) { if ( e instanceof ExactReadableEndedError && diff --git a/libraries/adb/src/commands/subprocess/protocols/shell.ts b/libraries/adb/src/commands/subprocess/protocols/shell.ts index ffabf3c6..f6a66e3f 100644 --- a/libraries/adb/src/commands/subprocess/protocols/shell.ts +++ b/libraries/adb/src/commands/subprocess/protocols/shell.ts @@ -139,14 +139,12 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol { this.#stdin = new MaybeConsumable.WritableStream({ write: async (chunk) => { - await MaybeConsumable.tryConsume(chunk, async (chunk) => { - await this.#writer.write( - AdbShellProtocolPacket.serialize({ - id: AdbShellProtocolId.Stdin, - data: chunk, - }), - ); - }); + await this.#writer.write( + AdbShellProtocolPacket.serialize({ + id: AdbShellProtocolId.Stdin, + data: chunk, + }), + ); }, }); } diff --git a/libraries/adb/src/commands/sync/socket.ts b/libraries/adb/src/commands/sync/socket.ts index 15ff938a..3ef72429 100644 --- a/libraries/adb/src/commands/sync/socket.ts +++ b/libraries/adb/src/commands/sync/socket.ts @@ -35,7 +35,8 @@ export class AdbSyncSocketLocked implements AsyncExactReadable { this.#combiner = new BufferCombiner(bufferSize); } - async #writeConsumable(buffer: Uint8Array) { + async #write(buffer: Uint8Array) { + // `#combiner` will reuse the buffer, so we need to use the Consumable pattern await Consumable.WritableStream.write(this.#writer, buffer); } @@ -44,7 +45,7 @@ export class AdbSyncSocketLocked implements AsyncExactReadable { await this.#writeLock.wait(); const buffer = this.#combiner.flush(); if (buffer) { - await this.#writeConsumable(buffer); + await this.#write(buffer); } } finally { this.#writeLock.notifyOne(); @@ -55,7 +56,7 @@ export class AdbSyncSocketLocked implements AsyncExactReadable { try { await this.#writeLock.wait(); for (const buffer of this.#combiner.push(data)) { - await this.#writeConsumable(buffer); + await this.#write(buffer); } } finally { this.#writeLock.notifyOne(); @@ -63,11 +64,15 @@ export class AdbSyncSocketLocked implements AsyncExactReadable { } async readExactly(length: number) { + // The request may still be in the internal buffer. + // Call `flush` to send it before starting reading await this.flush(); return await this.#readable.readExactly(length); } release(): void { + // In theory, the writer shouldn't leave anything in the buffer, + // but to be safe, call `flush` to throw away any remaining data. this.#combiner.flush(); this.#socketLock.notifyOne(); } diff --git a/libraries/adb/src/daemon/dispatcher.ts b/libraries/adb/src/daemon/dispatcher.ts index 12989473..aedcb31a 100644 --- a/libraries/adb/src/daemon/dispatcher.ts +++ b/libraries/adb/src/daemon/dispatcher.ts @@ -24,27 +24,56 @@ import { AdbCommand, calculateChecksum } from "./packet.js"; import { AdbDaemonSocketController } from "./socket.js"; export interface AdbPacketDispatcherOptions { - calculateChecksum: boolean; /** - * Before Android 9.0, ADB uses `char*` to parse service string, + * From Android 9.0, ADB stopped checking the checksum in packet header to improve performance. + * + * The value should be inferred from the device's ADB protocol version. + */ + calculateChecksum: boolean; + + /** + * Before Android 9.0, ADB uses `char*` to parse service strings, * thus requires a null character to terminate. * - * Usually it should have the same value as `calculateChecksum`. + * The value should be inferred from the device's ADB protocol version. + * Usually it should have the same value as `calculateChecksum`, since they both changed + * in Android 9.0. */ appendNullToServiceString: boolean; - maxPayloadSize: number; - /** - * The number of bytes the device can send before receiving an ack packet. - * Set to 0 or any negative value to disable delayed ack. - * Otherwise the value must be in the range of unsigned 32-bit integer. - */ - initialDelayedAckBytes: number; + maxPayloadSize: number; + /** - * Whether to preserve the connection open after the `AdbPacketDispatcher` is closed. + * Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`) + * when `AdbPacketDispatcher.close` is called. + * + * @default false */ preserveConnection?: boolean | undefined; - debugSlowRead?: boolean | undefined; + + /** + * The number of bytes the device can send before receiving an ack packet. + * Using delayed ack can improve the throughput, + * especially when the device is connected over Wi-Fi (so the latency is higher). + * + * This must be the negotiated value between the client and device. If the device enabled + * delayed ack but the client didn't, the device will throw an error when the client sends + * the first `WRTE` packet. And vice versa. + */ + initialDelayedAckBytes: number; + + /** + * When set, the dispatcher will throw an error when + * one of the socket readable stalls for this amount of milliseconds. + * + * Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets. + * It's important to always read from all sockets to prevent stalling. + * + * This option is helpful to detect bugs in the client code. + * + * @default false + */ + readTimeLimit?: number | undefined; } interface SocketOpenResult { @@ -225,12 +254,19 @@ export class AdbPacketDispatcher implements Closeable { // Maybe the device is responding to a packet of last connection // Tell the device to close the socket - void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); + void this.sendPacket( + AdbCommand.Close, + packet.arg1, + packet.arg0, + EMPTY_UINT8_ARRAY, + ); } #sendOkay(localId: number, remoteId: number, ackBytes: number) { let payload: Uint8Array; if (this.options.initialDelayedAckBytes !== 0) { + // TODO: try reusing this buffer to reduce memory allocation + // However, that requires blocking reentrance of `sendOkay`, which might be more expensive payload = new Uint8Array(4); setUint32LittleEndian(payload, 0, ackBytes); } else { @@ -241,22 +277,24 @@ export class AdbPacketDispatcher implements Closeable { } async #handleOpen(packet: AdbPacketData) { - // `AsyncOperationManager` doesn't support skipping IDs - // Use `add` + `resolve` to simulate this behavior + // Allocate a local ID for the socket from `#initializers`. + // `AsyncOperationManager` doesn't directly support returning the next ID, + // so use `add` + `resolve` to simulate this const [localId] = this.#initializers.add(); this.#initializers.resolve(localId, undefined); const remoteId = packet.arg0; - let initialDelayedAckBytes = packet.arg1; + let availableWriteBytes = packet.arg1; const service = decodeUtf8(packet.payload); + // Check remote delayed ack enablement is consistent with local if (this.options.initialDelayedAckBytes === 0) { - if (initialDelayedAckBytes !== 0) { + if (availableWriteBytes !== 0) { throw new Error("Invalid OPEN packet. arg1 should be 0"); } - initialDelayedAckBytes = Infinity; + availableWriteBytes = Infinity; } else { - if (initialDelayedAckBytes === 0) { + if (availableWriteBytes === 0) { throw new Error( "Invalid OPEN packet. arg1 should be greater than 0", ); @@ -265,7 +303,12 @@ export class AdbPacketDispatcher implements Closeable { const handler = this.#incomingSocketHandlers.get(service); if (!handler) { - await this.sendPacket(AdbCommand.Close, 0, remoteId); + await this.sendPacket( + AdbCommand.Close, + 0, + remoteId, + EMPTY_UINT8_ARRAY, + ); return; } @@ -275,8 +318,8 @@ export class AdbPacketDispatcher implements Closeable { remoteId, localCreated: false, service, + availableWriteBytes, }); - controller.ack(initialDelayedAckBytes); try { await handler(controller.socket); @@ -287,7 +330,12 @@ export class AdbPacketDispatcher implements Closeable { this.options.initialDelayedAckBytes, ); } catch (e) { - await this.sendPacket(AdbCommand.Close, 0, remoteId); + await this.sendPacket( + AdbCommand.Close, + 0, + remoteId, + EMPTY_UINT8_ARRAY, + ); } } @@ -298,14 +346,8 @@ export class AdbPacketDispatcher implements Closeable { } let handled = false; - await Promise.race([ - delay(5000).then(() => { - if (this.options.debugSlowRead && !handled) { - throw new Error( - `packet for \`${socket.service}\` not handled in 5 seconds`, - ); - } - }), + + const promises: Promise[] = [ (async () => { await socket.enqueue(packet.payload); await this.#sendOkay( @@ -315,9 +357,22 @@ export class AdbPacketDispatcher implements Closeable { ); handled = true; })(), - ]); + ]; - return; + if (this.options.readTimeLimit) { + promises.push( + (async () => { + await delay(this.options.readTimeLimit!); + if (!handled) { + throw new Error( + `readable of \`${socket.service}\` has stalled for ${this.options.readTimeLimit} milliseconds`, + ); + } + })(), + ); + } + + await Promise.race(promises); } async createSocket(service: string): Promise { @@ -342,8 +397,8 @@ export class AdbPacketDispatcher implements Closeable { remoteId, localCreated: true, service, + availableWriteBytes, }); - controller.ack(availableWriteBytes); this.#sockets.set(localId, controller); return controller.socket; @@ -365,7 +420,8 @@ export class AdbPacketDispatcher implements Closeable { command: AdbCommand, arg0: number, arg1: number, - payload: string | Uint8Array = EMPTY_UINT8_ARRAY, + // PERF: It's slightly faster to not use default parameter values + payload: string | Uint8Array, ): Promise { if (typeof payload === "string") { payload = encodeUtf8(payload); diff --git a/libraries/adb/src/daemon/packet.ts b/libraries/adb/src/daemon/packet.ts index 051acf30..0cfb0c94 100644 --- a/libraries/adb/src/daemon/packet.ts +++ b/libraries/adb/src/daemon/packet.ts @@ -1,8 +1,4 @@ -import type { Consumable } from "@yume-chan/stream-extra"; -import { - ConsumableReadableStream, - TransformStream, -} from "@yume-chan/stream-extra"; +import { Consumable, TransformStream } from "@yume-chan/stream-extra"; import Struct from "@yume-chan/struct"; export enum AdbCommand { @@ -65,7 +61,7 @@ export class AdbPacketSerializeStream extends TransformStream< const init = chunk as AdbPacketInit & AdbPacketHeaderInit; init.payloadLength = init.payload.byteLength; - await ConsumableReadableStream.enqueue( + await Consumable.ReadableStream.enqueue( controller, AdbPacketHeader.serialize(init, headerBuffer), ); @@ -74,7 +70,7 @@ export class AdbPacketSerializeStream extends TransformStream< // USB protocol preserves packet boundaries, // so we must write payload separately as native ADB does, // otherwise the read operation on device will fail. - await ConsumableReadableStream.enqueue( + await Consumable.ReadableStream.enqueue( controller, init.payload, ); diff --git a/libraries/adb/src/daemon/socket.ts b/libraries/adb/src/daemon/socket.ts index d3937d39..7c24f53d 100644 --- a/libraries/adb/src/daemon/socket.ts +++ b/libraries/adb/src/daemon/socket.ts @@ -9,6 +9,7 @@ import { type WritableStream, type WritableStreamDefaultController, } from "@yume-chan/stream-extra"; +import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct"; import type { AdbSocket } from "../adb.js"; @@ -23,11 +24,15 @@ export interface AdbDaemonSocketInfo { service: string; } -export interface AdbDaemonSocketConstructionOptions - extends AdbDaemonSocketInfo { +export interface AdbDaemonSocketInit extends AdbDaemonSocketInfo { dispatcher: AdbPacketDispatcher; highWaterMark?: number | undefined; + + /** + * The initial delayed ack byte count, or `Infinity` if delayed ack is disabled. + */ + availableWriteBytes: number; } export class AdbDaemonSocketController @@ -72,7 +77,7 @@ export class AdbDaemonSocketController */ #availableWriteBytes = 0; - constructor(options: AdbDaemonSocketConstructionOptions) { + constructor(options: AdbDaemonSocketInit) { this.#dispatcher = options.dispatcher; this.localId = options.localId; this.remoteId = options.remoteId; @@ -102,6 +107,7 @@ export class AdbDaemonSocketController }); this.#socket = new AdbDaemonSocket(this); + this.#availableWriteBytes = options.availableWriteBytes; } async #writeChunk(data: Uint8Array, signal: AbortSignal) { @@ -172,6 +178,7 @@ export class AdbDaemonSocketController AdbCommand.Close, this.localId, this.remoteId, + EMPTY_UINT8_ARRAY, ); } diff --git a/libraries/adb/src/daemon/transport.ts b/libraries/adb/src/daemon/transport.ts index d1be3975..f6c0787f 100644 --- a/libraries/adb/src/daemon/transport.ts +++ b/libraries/adb/src/daemon/transport.ts @@ -56,26 +56,52 @@ export type AdbDaemonConnection = ReadableWritablePair< Consumable >; -interface AdbDaemonAuthenticationOptions { +export interface AdbDaemonAuthenticationOptions { serial: string; connection: AdbDaemonConnection; credentialStore: AdbCredentialStore; authenticators?: AdbAuthenticator[]; features?: readonly AdbFeature[]; + /** * The number of bytes the device can send before receiving an ack packet. + * Using delayed ack can improve the throughput, + * especially when the device is connected over Wi-Fi (so the latency is higher). * * Set to 0 or any negative value to disable delayed ack in handshake. * Otherwise the value must be in the range of unsigned 32-bit integer. * - * Delayed ack requires Android 14, this option is ignored on older versions. + * Delayed ack was added in Android 14, + * this option will be ignored when the device doesn't support it. + * + * @default ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE */ initialDelayedAckBytes?: number; + /** - * Whether to preserve the connection open after the `AdbDaemonTransport` is closed. + * Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`) + * when `AdbDaemonTransport.close` is called. + * + * Note that when `authenticate` fails, + * no matter which value this option has, + * the `connection` is always kept open, so it can be used in another `authenticate` call. + * + * @default false */ preserveConnection?: boolean | undefined; - debugSlowRead?: boolean | undefined; + + /** + * When set, the transport will throw an error when + * one of the socket readable stalls for this amount of milliseconds. + * + * Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets. + * It's important to always read from all sockets to prevent stalling. + * + * This option is helpful to detect bugs in the client code. + * + * @default undefined + */ + readTimeLimit?: number | undefined; } interface AdbDaemonSocketConnectorConstructionOptions { @@ -85,20 +111,39 @@ interface AdbDaemonSocketConnectorConstructionOptions { maxPayloadSize: number; banner: string; features?: readonly AdbFeature[]; + /** * The number of bytes the device can send before receiving an ack packet. + * Using delayed ack can improve the throughput, + * especially when the device is connected over Wi-Fi (so the latency is higher). * - * Set to 0 or any negative value to disable delayed ack in handshake. - * Otherwise the value must be in the range of unsigned 32-bit integer. - * - * Delayed ack requires Android 14, this option is ignored on older versions. + * When `features` doesn't include `AdbFeature.DelayedAck`, it must be set to 0. Otherwise, + * the value must be in the range of unsigned 32-bit integer. If the device enabled + * delayed ack but the client didn't, the device will throw an error when the client sends + * the first data packet. And vice versa. */ - initialDelayedAckBytes?: number; + initialDelayedAckBytes: number; + /** - * Whether to preserve the connection open after the `AdbDaemonTransport` is closed. + * Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`) + * when `AdbDaemonTransport.close` is called. + * + * @default false */ preserveConnection?: boolean | undefined; - debugSlowRead?: boolean | undefined; + + /** + * When set, the transport will throw an error when + * one of the socket readable stalls for this amount of milliseconds. + * + * Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets. + * It's important to always read from all sockets to prevent stalling. + * + * This option is helpful to detect bugs in the client code. + * + * @default undefined + */ + readTimeLimit?: number | undefined; } export class AdbDaemonTransport implements AdbTransport { @@ -106,9 +151,9 @@ export class AdbDaemonTransport implements AdbTransport { * Authenticates the connection and creates an `AdbDaemonTransport` instance * that can be used by `Adb` class. * - * If an authentication process failed, it's possible to call `authenticate` again - * on the same connection. Because every time the device receives a `CNXN` packet, - * it resets all internal state, and starts a new authentication process. + * If an authentication process failed, + * no matter which value the `preserveConnection` option has, + * the `connection` is always kept open, so it can be used in another `authenticate` call. */ static async authenticate({ serial, @@ -278,7 +323,7 @@ export class AdbDaemonTransport implements AdbTransport { version, banner, features = ADB_DAEMON_DEFAULT_FEATURES, - initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, + initialDelayedAckBytes, ...options }: AdbDaemonSocketConnectorConstructionOptions) { this.#serial = serial; diff --git a/libraries/adb/src/server/client.ts b/libraries/adb/src/server/client.ts index 48da38d0..aaf47e80 100644 --- a/libraries/adb/src/server/client.ts +++ b/libraries/adb/src/server/client.ts @@ -27,7 +27,12 @@ import { import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js"; import { AdbBanner } from "../banner.js"; import type { AdbFeature } from "../features.js"; -import { NOOP, hexToNumber, numberToHex, unreachable } from "../utils/index.js"; +import { + NOOP, + hexToNumber, + unreachable, + write4HexDigits, +} from "../utils/index.js"; import { AdbServerTransport } from "./transport.js"; @@ -76,6 +81,23 @@ export interface AdbServerDevice { transportId: bigint; } +function sequenceEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +} + +const OKAY = encodeUtf8("OKAY"); +const FAIL = encodeUtf8("FAIL"); + export class AdbServerClient { static readonly VERSION = 41; @@ -99,8 +121,27 @@ export class AdbServerClient { return stream.readExactly(length); } }) - .then((valueBuffer) => { - return decodeUtf8(valueBuffer); + .then((buffer) => { + // TODO: Investigate using stream mode `TextDecoder` for long strings. + // Because concatenating strings uses rope data structure, + // which only points to the original strings and doesn't copy the data, + // it's more efficient than concatenating `Uint8Array`s. + // + // ``` + // const decoder = new TextDecoder(); + // let result = ''; + // for await (const chunk of stream.iterateExactly(length)) { + // result += decoder.decode(chunk, { stream: true }); + // } + // result += decoder.decode(); + // return result; + // ``` + // + // Although, it will be super complex to use `SyncPromise` with async iterator, + // `stream.iterateExactly` need to return an + // `Iterator>` instead of a true async iterator. + // Maybe `SyncPromise` should support async iterators directly. + return decodeUtf8(buffer); }) .valueOrPromise(); } @@ -109,27 +150,29 @@ export class AdbServerClient { writer: WritableStreamDefaultWriter, value: string, ): Promise { - const valueBuffer = encodeUtf8(value); - const buffer = new Uint8Array(4 + valueBuffer.length); - buffer.set(numberToHex(valueBuffer.length)); - buffer.set(valueBuffer, 4); + // TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length + // That way allocates a new string (hopefully only a rope) instead of a new buffer + const encoded = encodeUtf8(value); + const buffer = new Uint8Array(4 + encoded.length); + write4HexDigits(buffer, 0, encoded.length); + buffer.set(encoded, 4); await writer.write(buffer); } static async readOkay( stream: ExactReadable | AsyncExactReadable, ): Promise { - const response = decodeUtf8(await stream.readExactly(4)); - if (response === "OKAY") { + const response = await stream.readExactly(4); + if (sequenceEqual(response, OKAY)) { return; } - if (response === "FAIL") { + if (sequenceEqual(response, FAIL)) { const reason = await AdbServerClient.readString(stream); throw new Error(reason); } - throw new Error(`Unexpected response: ${response}`); + throw new Error(`Unexpected response: ${decodeUtf8(response)}`); } async connect( diff --git a/libraries/adb/src/utils/base64.ts b/libraries/adb/src/utils/base64.ts index 8295b927..88c61503 100644 --- a/libraries/adb/src/utils/base64.ts +++ b/libraries/adb/src/utils/base64.ts @@ -79,40 +79,51 @@ export function encodeBase64( output.byteOffset + output.length - (paddingLength + 1) <= input.byteOffset + input.length ) { - // Output ends before input ends - // So output won't catch up with input. + // Output ends before input ends. + // Can encode forwards, because writing output won't catch up with reading input. - // Depends on padding length, - // it's possible to write 1-3 bytes after input ends. - // spell: disable-next-line - // | aaaaaabb | | | | - // | aaaaaa | bb0000 | = | = | + // The output end is subtracted by `(paddingLength + 1)` because + // depending on padding length, it's possible to write 1-3 extra bytes after input ends. // - // spell: disable-next-line - // | aaaaaabb | bbbbcccc | | | - // | aaaaaa | bbbbbb | cccc00 | = | + // The following diagrams show how the last read from input and the last write to output + // are not conflicting. // - // spell: disable-next-line - // | aaaaaabb | bbbbcccc | ccdddddd | | - // | aaaaaa | bbbbbb | cccccc | dddddd | + // spell: disable + // + // `paddingLength === 2` can write 3 extra bytes: + // + // input: | aaaaaabb | | | | + // output: | aaaaaa | bb0000 | = | = | + // + // `paddingLength === 1` can write 2 extra bytes: + // + // input: | aaaaaabb | bbbbcccc | | | + // output: | aaaaaa | bbbbbb | cccc00 | = | + // + // `paddingLength === 0` can write 1 extra byte: + // + // input: | aaaaaabb | bbbbcccc | ccdddddd | | + // output: | aaaaaa | bbbbbb | cccccc | dddddd | + // + // spell: enable - // Must encode forwards. encodeForward(input, output, paddingLength); } else if (output.byteOffset >= input.byteOffset - 1) { // Output starts after input starts - // So in backwards, output can't catch up with input. + // So in backwards, writing output won't catch up with reading input. - // Because first 3 bytes becomes 4 bytes, - // it's possible to write 1 byte before input starts. + // The input start is subtracted by `1`, Because as the first 3 bytes becomes 4 bytes, + // it's possible to write 1 extra byte before input starts. // spell: disable-next-line - // | | aaaaaabb | bbbbcccc | ccdddddd | - // | aaaaaa | bbbbbb | cccccc | dddddd | + // input: | | aaaaaabb | bbbbcccc | ccdddddd | + // output: | aaaaaa | bbbbbb | cccccc | dddddd | // Must encode backwards. encodeBackward(input, output, paddingLength); } else { // Input is in the middle of output, - // not possible to read neither first or last three bytes, + // It's not possible to read either the first or the last three bytes + // before they are overwritten by the output. throw new Error("input and output cannot overlap"); } diff --git a/libraries/adb/src/utils/hex.ts b/libraries/adb/src/utils/hex.ts index 736433c1..1f8994bf 100644 --- a/libraries/adb/src/utils/hex.ts +++ b/libraries/adb/src/utils/hex.ts @@ -36,23 +36,25 @@ export function hexToNumber(data: Uint8Array): number { return result; } -export function numberToHex(value: number) { - const result = new Uint8Array(4); - let index = 3; - while (index >= 0 && value > 0) { +export function write4HexDigits( + buffer: Uint8Array, + index: number, + value: number, +) { + const start = index; + index += 3; + while (index >= start && value > 0) { const digit = value & 0xf; value >>= 4; if (digit < 10) { - result[index] = digit + 48; + buffer[index] = digit + 48; // '0' } else { - result[index] = digit + 87; + buffer[index] = digit + 87; // 'a' - 10 } index -= 1; } - while (index >= 0) { - // '0' - result[index] = 48; + while (index >= start) { + buffer[index] = 48; // '0' index -= 1; } - return result; } diff --git a/libraries/android-bin/src/logcat.ts b/libraries/android-bin/src/logcat.ts index 947f7209..b7473442 100644 --- a/libraries/android-bin/src/logcat.ts +++ b/libraries/android-bin/src/logcat.ts @@ -353,6 +353,7 @@ export async function deserializeAndroidLogEntry( ): Promise { const entry = (await LoggerEntry.deserialize(stream)) as AndroidLogEntry; if (entry.headerSize !== LoggerEntry.size) { + // Skip unknown fields await stream.readExactly(entry.headerSize - LoggerEntry.size); } diff --git a/libraries/fetch-scrcpy-server/package.json b/libraries/fetch-scrcpy-server/package.json index 3c49039e..f597edab 100644 --- a/libraries/fetch-scrcpy-server/package.json +++ b/libraries/fetch-scrcpy-server/package.json @@ -30,6 +30,6 @@ "gh-release-fetch": "^4.0.3" }, "devDependencies": { - "@types/node": "^20.12.7" + "@types/node": "^20.12.8" } } diff --git a/libraries/no-data-view/package.json b/libraries/no-data-view/package.json index e450234c..bcf64a48 100644 --- a/libraries/no-data-view/package.json +++ b/libraries/no-data-view/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", - "@types/node": "^20.12.7", + "@types/node": "^20.12.8", "@yume-chan/eslint-config": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0", "cross-env": "^7.0.3", diff --git a/libraries/scrcpy-decoder-webcodecs/src/index.ts b/libraries/scrcpy-decoder-webcodecs/src/index.ts index 5528843d..e1607b33 100644 --- a/libraries/scrcpy-decoder-webcodecs/src/index.ts +++ b/libraries/scrcpy-decoder-webcodecs/src/index.ts @@ -344,8 +344,11 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder { return; } - // WebCodecs requires configuration data to be with the first frame. + // For H.264 and H.265, when the stream is in Annex B format + // (which Scrcpy uses, as Android MediaCodec produces), + // configuration data needs to be combined with the first frame data. // https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type + // AV1 doesn't need to do this, the handling code also doesn't set `#config`. let data: Uint8Array; if (this.#config !== undefined) { data = new Uint8Array( diff --git a/libraries/scrcpy/src/codec/nalu.ts b/libraries/scrcpy/src/codec/nalu.ts index 73db07d4..9cd7f44e 100644 --- a/libraries/scrcpy/src/codec/nalu.ts +++ b/libraries/scrcpy/src/codec/nalu.ts @@ -91,145 +91,6 @@ export function* annexBSplitNalu(buffer: Uint8Array): Generator { yield buffer.subarray(start, buffer.length); } -/** - * Remove emulation prevention bytes from a H.264/H.265 NAL Unit. - * - * The input is not modified. - * If the input doesn't contain any emulation prevention bytes, - * the input is returned as-is. - * Otherwise, a new `Uint8Array` is created and returned. - */ -export function naluRemoveEmulation(buffer: Uint8Array) { - // output will be created when first emulation prevention byte is found - let output: Uint8Array | undefined; - let outputOffset = 0; - - let zeroCount = 0; - let inEmulation = false; - - let i = 0; - scan: for (; i < buffer.length; i += 1) { - const byte = buffer[i]!; - - if (byte === 0x00) { - zeroCount += 1; - continue; - } - - // Current byte is not zero - const prevZeroCount = zeroCount; - zeroCount = 0; - - if (prevZeroCount < 2) { - // zero or one `0x00`s are acceptable - continue; - } - - if (byte === 0x01) { - // Unexpected start code - throw new Error("Invalid data"); - } - - if (prevZeroCount > 2) { - // Too much `0x00`s - throw new Error("Invalid data"); - } - - switch (byte) { - case 0x02: - // Didn't find why, but 7.4.1 NAL unit semantics forbids `0x000002` appearing in NAL units - throw new Error("Invalid data"); - case 0x03: - // `0x000003` is the "emulation_prevention_three_byte" - // `0x00000300`, `0x00000301`, `0x00000302` and `0x00000303` represent - // `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively - inEmulation = true; - - // Create output and copy the data before the emulation prevention byte - // Output size is unknown, so we use the input size as an upper bound - output = new Uint8Array(buffer.length - 1); - output.set(buffer.subarray(0, i)); - outputOffset = i; - i += 1; - break scan; - default: - // `0x000004` or larger are as-is - break; - } - } - - if (!output) { - return buffer; - } - - // Continue at the byte after the emulation prevention byte - for (; i < buffer.length; i += 1) { - const byte = buffer[i]!; - - output[outputOffset] = byte; - outputOffset += 1; - - if (inEmulation) { - if (byte > 0x03) { - // `0x00000304` or larger are invalid - throw new Error("Invalid data"); - } - - // `00000300000300` results in `0000000000` (both `0x03` are removed) - // which means the `0x00` after `0x03` also counts - if (byte === 0x00) { - zeroCount += 1; - } - - inEmulation = false; - continue; - } - - if (byte === 0x00) { - zeroCount += 1; - continue; - } - - const prevZeroCount = zeroCount; - zeroCount = 0; - - if (prevZeroCount < 2) { - // zero or one `0x00`s are acceptable - continue; - } - - if (byte === 0x01) { - // Unexpected start code - throw new Error("Invalid data"); - } - - if (prevZeroCount > 2) { - // Too much `0x00`s - throw new Error("Invalid data"); - } - - switch (byte) { - case 0x02: - // Didn't find why, but 7.4.1 NAL unit semantics forbids `0x000002` appearing in NAL units - throw new Error("Invalid data"); - case 0x03: - // `0x000003` is the "emulation_prevention_three_byte" - // `0x00000300`, `0x00000301`, `0x00000302` and `0x00000303` represent - // `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively - inEmulation = true; - - // Remove the emulation prevention byte - outputOffset -= 1; - break; - default: - // `0x000004` or larger are as-is - break; - } - } - - return output.subarray(0, outputOffset); -} - export class NaluSodbBitReader { readonly #nalu: Uint8Array; // logical length is `#byteLength * 8 + (7 - #stopBitIndex)` @@ -269,6 +130,7 @@ export class NaluSodbBitReader { constructor(nalu: Uint8Array) { this.#nalu = nalu; + // Search for the last bit being `1`, also known as the stop bit for (let i = nalu.length - 1; i >= 0; i -= 1) { if (this.#nalu[i] === 0) { continue; @@ -292,16 +154,18 @@ export class NaluSodbBitReader { this.#byte = this.#nalu[this.#bytePosition]!; // If the current sequence is `0x000003`, skip to the next byte. - // `annexBSplitNalu` had validated the input, so don't need to check here. + // `annexBSplitNalu` had validated the input, so skip the check here if (this.#zeroCount === 2 && this.#byte === 3) { this.#zeroCount = 0; this.#bytePosition += 1; + // Call `#loadByte` again, because if the next byte is `0x00`, + // it need to be counted in `#zeroCount` as well. this.#loadByte(); return; } // `0x00000301` becomes `0x000001`, so only the `0x03` byte needs to be skipped - // The `0x00` bytes are still returned as-is + // All `0x00` bytes are returned as-is if (this.#byte === 0) { this.#zeroCount += 1; } else { @@ -338,7 +202,13 @@ export class NaluSodbBitReader { return result; } - #ensurePositionValid() { + #checkSkipPosition() { + // This is different from `ended`, + // as it allows the bit position to be at the stop bit. + // In this case, there is no more bits to read, `ended` is `true`, + // and the next `next` call will throw an error. + // However, it's still a valid position for `skip`, which can skip all remaining bits, + // and stop at the end position. if ( this.#bytePosition >= this.#byteLength && this.#bitPosition < this.#stopBitIndex @@ -350,24 +220,29 @@ export class NaluSodbBitReader { skip(length: number) { if (length <= this.#bitPosition + 1) { this.#bitPosition -= length; - this.#ensurePositionValid(); + this.#checkSkipPosition(); return; } + // Because of emulation prevention bytes, + // we don't know how many bits are left in the NAL, + // nor how many bits should be skipped. + // So we need to check each byte. + length -= this.#bitPosition + 1; this.#bytePosition += 1; this.#bitPosition = 7; this.#loadByte(); - this.#ensurePositionValid(); + this.#checkSkipPosition(); for (; length >= 8; length -= 8) { this.#bytePosition += 1; this.#loadByte(); - this.#ensurePositionValid(); + this.#checkSkipPosition(); } this.#bitPosition = 7 - length; - this.#ensurePositionValid(); + this.#checkSkipPosition(); } decodeExponentialGolombNumber(): number { diff --git a/libraries/scrcpy/src/options/1_16/float-to-uint16.ts b/libraries/scrcpy/src/options/1_16/float.ts similarity index 81% rename from libraries/scrcpy/src/options/1_16/float-to-uint16.ts rename to libraries/scrcpy/src/options/1_16/float.ts index 7986efbe..2c7df0d1 100644 --- a/libraries/scrcpy/src/options/1_16/float-to-uint16.ts +++ b/libraries/scrcpy/src/options/1_16/float.ts @@ -1,5 +1,5 @@ import { getUint16 } from "@yume-chan/no-data-view"; -import type { NumberFieldType } from "@yume-chan/struct"; +import type { NumberFieldVariant } from "@yume-chan/struct"; import { NumberFieldDefinition } from "@yume-chan/struct"; export function clamp(value: number, min: number, max: number): number { @@ -14,7 +14,7 @@ export function clamp(value: number, min: number, max: number): number { return value; } -export const ScrcpyFloatToUint16NumberType: NumberFieldType = { +export const ScrcpyUnsignedFloatNumberVariant: NumberFieldVariant = { size: 2, signed: false, deserialize(array, littleEndian) { @@ -30,6 +30,6 @@ export const ScrcpyFloatToUint16NumberType: NumberFieldType = { }, }; -export const ScrcpyFloatToUint16FieldDefinition = new NumberFieldDefinition( - ScrcpyFloatToUint16NumberType, +export const ScrcpyUnsignedFloatFieldDefinition = new NumberFieldDefinition( + ScrcpyUnsignedFloatNumberVariant, ); diff --git a/libraries/scrcpy/src/options/1_16/index.ts b/libraries/scrcpy/src/options/1_16/index.ts index 39455069..d8cc9683 100644 --- a/libraries/scrcpy/src/options/1_16/index.ts +++ b/libraries/scrcpy/src/options/1_16/index.ts @@ -1,5 +1,5 @@ export * from "./codec-options.js"; -export * from "./float-to-uint16.js"; +export * from "./float.js"; export * from "./init.js"; export * from "./message.js"; export * from "./options.js"; diff --git a/libraries/scrcpy/src/options/1_16/message.ts b/libraries/scrcpy/src/options/1_16/message.ts index e00bb68a..1125501b 100644 --- a/libraries/scrcpy/src/options/1_16/message.ts +++ b/libraries/scrcpy/src/options/1_16/message.ts @@ -6,7 +6,7 @@ import { ScrcpyControlMessageType, } from "../../control/index.js"; -import { ScrcpyFloatToUint16FieldDefinition } from "./float-to-uint16.js"; +import { ScrcpyUnsignedFloatFieldDefinition } from "./float.js"; export const SCRCPY_CONTROL_MESSAGE_TYPES_1_16: readonly ScrcpyControlMessageType[] = [ @@ -38,7 +38,7 @@ export const ScrcpyInjectTouchControlMessage1_16 = new Struct() .uint32("pointerY") .uint16("screenWidth") .uint16("screenHeight") - .field("pressure", ScrcpyFloatToUint16FieldDefinition) + .field("pressure", ScrcpyUnsignedFloatFieldDefinition) .uint32("buttons"); export type ScrcpyInjectTouchControlMessage1_16 = diff --git a/libraries/scrcpy/src/options/1_16/options.ts b/libraries/scrcpy/src/options/1_16/options.ts index eaf1d391..054fcc4a 100644 --- a/libraries/scrcpy/src/options/1_16/options.ts +++ b/libraries/scrcpy/src/options/1_16/options.ts @@ -83,13 +83,20 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions { return order.map((key) => toScrcpyOptionValue(options[key], "-")); } + /** + * Parse a fixed-length, null-terminated string. + * @param stream The stream to read from + * @param maxLength The maximum length of the string, including the null terminator, in bytes + * @returns The parsed string, without the null terminator + */ static async parseCString( stream: AsyncExactReadable, maxLength: number, ): Promise { - let result = decodeUtf8(await stream.readExactly(maxLength)); - result = result.substring(0, result.indexOf("\0")); - return result; + const buffer = await stream.readExactly(maxLength); + // If null terminator is not found, `subarray(0, -1)` will remove the last byte + // But since it's a invalid case, it's fine + return decodeUtf8(buffer.subarray(0, buffer.indexOf(0))); } static async parseUint16BE(stream: AsyncExactReadable): Promise { diff --git a/libraries/scrcpy/src/options/1_25/scroll.spec.ts b/libraries/scrcpy/src/options/1_25/scroll.spec.ts index 70b8fc60..53c6231b 100644 --- a/libraries/scrcpy/src/options/1_25/scroll.spec.ts +++ b/libraries/scrcpy/src/options/1_25/scroll.spec.ts @@ -3,29 +3,29 @@ import { describe, expect, it } from "@jest/globals"; import { ScrcpyControlMessageType } from "../../control/index.js"; import { - ScrcpyFloatToInt16NumberType, ScrcpyScrollController1_25, + ScrcpySignedFloatNumberVariant, } from "./scroll.js"; describe("ScrcpyFloatToInt16NumberType", () => { it("should serialize", () => { const dataView = new DataView(new ArrayBuffer(2)); - ScrcpyFloatToInt16NumberType.serialize(dataView, 0, -1, true); + ScrcpySignedFloatNumberVariant.serialize(dataView, 0, -1, true); expect(dataView.getInt16(0, true)).toBe(-0x8000); - ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 0, true); + ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 0, true); expect(dataView.getInt16(0, true)).toBe(0); - ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 1, true); + ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 1, true); expect(dataView.getInt16(0, true)).toBe(0x7fff); }); it("should clamp input values", () => { const dataView = new DataView(new ArrayBuffer(2)); - ScrcpyFloatToInt16NumberType.serialize(dataView, 0, -2, true); + ScrcpySignedFloatNumberVariant.serialize(dataView, 0, -2, true); expect(dataView.getInt16(0, true)).toBe(-0x8000); - ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 2, true); + ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 2, true); expect(dataView.getInt16(0, true)).toBe(0x7fff); }); @@ -34,13 +34,13 @@ describe("ScrcpyFloatToInt16NumberType", () => { const view = new Uint8Array(dataView.buffer); dataView.setInt16(0, -0x8000, true); - expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(-1); + expect(ScrcpySignedFloatNumberVariant.deserialize(view, true)).toBe(-1); dataView.setInt16(0, 0, true); - expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(0); + expect(ScrcpySignedFloatNumberVariant.deserialize(view, true)).toBe(0); dataView.setInt16(0, 0x7fff, true); - expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(1); + expect(ScrcpySignedFloatNumberVariant.deserialize(view, true)).toBe(1); }); }); diff --git a/libraries/scrcpy/src/options/1_25/scroll.ts b/libraries/scrcpy/src/options/1_25/scroll.ts index 118ddc11..3bcce7f4 100644 --- a/libraries/scrcpy/src/options/1_25/scroll.ts +++ b/libraries/scrcpy/src/options/1_25/scroll.ts @@ -1,5 +1,5 @@ import { getInt16 } from "@yume-chan/no-data-view"; -import type { NumberFieldType } from "@yume-chan/struct"; +import type { NumberFieldVariant } from "@yume-chan/struct"; import Struct, { NumberFieldDefinition } from "@yume-chan/struct"; import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js"; @@ -7,7 +7,7 @@ import { ScrcpyControlMessageType } from "../../control/index.js"; import type { ScrcpyScrollController } from "../1_16/index.js"; import { clamp } from "../1_16/index.js"; -export const ScrcpyFloatToInt16NumberType: NumberFieldType = { +export const ScrcpySignedFloatNumberVariant: NumberFieldVariant = { size: 2, signed: true, deserialize(array, littleEndian) { @@ -23,8 +23,8 @@ export const ScrcpyFloatToInt16NumberType: NumberFieldType = { }, }; -const ScrcpyFloatToInt16FieldDefinition = new NumberFieldDefinition( - ScrcpyFloatToInt16NumberType, +const ScrcpySignedFloatFieldDefinition = new NumberFieldDefinition( + ScrcpySignedFloatNumberVariant, ); export const ScrcpyInjectScrollControlMessage1_25 = new Struct() @@ -33,8 +33,8 @@ export const ScrcpyInjectScrollControlMessage1_25 = new Struct() .uint32("pointerY") .uint16("screenWidth") .uint16("screenHeight") - .field("scrollX", ScrcpyFloatToInt16FieldDefinition) - .field("scrollY", ScrcpyFloatToInt16FieldDefinition) + .field("scrollX", ScrcpySignedFloatFieldDefinition) + .field("scrollY", ScrcpySignedFloatFieldDefinition) .int32("buttons"); export type ScrcpyInjectScrollControlMessage1_25 = diff --git a/libraries/scrcpy/src/options/2_0.ts b/libraries/scrcpy/src/options/2_0.ts index e7c134a8..8a8926a4 100644 --- a/libraries/scrcpy/src/options/2_0.ts +++ b/libraries/scrcpy/src/options/2_0.ts @@ -14,8 +14,8 @@ import type { import { CodecOptions, - ScrcpyFloatToUint16FieldDefinition, ScrcpyOptions1_16, + ScrcpyUnsignedFloatFieldDefinition, } from "./1_16/index.js"; import { ScrcpyOptions1_21 } from "./1_21.js"; import type { ScrcpyOptionsInit1_24 } from "./1_24.js"; @@ -38,7 +38,7 @@ export const ScrcpyInjectTouchControlMessage2_0 = new Struct() .uint32("pointerY") .uint16("screenWidth") .uint16("screenHeight") - .field("pressure", ScrcpyFloatToUint16FieldDefinition) + .field("pressure", ScrcpyUnsignedFloatFieldDefinition) .uint32("actionButton") .uint32("buttons"); @@ -107,6 +107,58 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase< ScrcpyOptionsInit2_0, ScrcpyOptions1_25 > { + static async parseAudioMetadata( + stream: ReadableStream, + sendCodecMeta: boolean, + mapMetadata: (value: number) => ScrcpyAudioCodec, + getOptionCodec: () => ScrcpyAudioCodec, + ): Promise { + 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(async (controller) => { + // Put the first 4 bytes back + await controller.enqueue(buffer); + + const stream = buffered.release(); + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + await controller.enqueue(value); + } + }), + }; + } + static readonly DEFAULTS = { ...omit(ScrcpyOptions1_24.DEFAULTS, [ "bitRate", @@ -255,81 +307,34 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase< override parseAudioStreamMetadata( stream: ReadableStream, ): ValueOrPromise { - return (async (): Promise => { - const buffered = new BufferedReadableStream(stream); - const buffer = await buffered.readExactly(4); - - const codecMetadataValue = getUint32BigEndian(buffer, 0); - // Server will send `0x00_00_00_00` and `0x00_00_00_01` even if `sendCodecMeta` is false - switch (codecMetadataValue) { - case 0x00_00_00_00: - return { - type: "disabled", - }; - case 0x00_00_00_01: - return { - type: "errored", - }; - } - - if (this.value.sendCodecMeta) { - let codec: ScrcpyAudioCodec; - switch (codecMetadataValue) { - case ScrcpyAudioCodec.OPUS.metadataValue: - codec = ScrcpyAudioCodec.OPUS; - break; - case ScrcpyAudioCodec.AAC.metadataValue: - codec = ScrcpyAudioCodec.AAC; - break; + return ScrcpyOptions2_0.parseAudioMetadata( + stream, + this.value.sendCodecMeta, + (value) => { + switch (value) { case ScrcpyAudioCodec.RAW.metadataValue: - codec = ScrcpyAudioCodec.RAW; - break; + return ScrcpyAudioCodec.RAW; + case ScrcpyAudioCodec.OPUS.metadataValue: + return ScrcpyAudioCodec.OPUS; + case ScrcpyAudioCodec.AAC.metadataValue: + return ScrcpyAudioCodec.AAC; default: throw new Error( - `Unknown audio codec metadata value: ${codecMetadataValue}`, + `Unknown audio codec metadata value: ${value}`, ); } - return { - type: "success", - codec, - stream: buffered.release(), - }; - } - - // Infer codec from `audioCodec` option - let codec: ScrcpyAudioCodec; - switch (this.value.audioCodec) { - case "opus": - codec = ScrcpyAudioCodec.OPUS; - break; - case "aac": - codec = ScrcpyAudioCodec.AAC; - break; - case "raw": - codec = ScrcpyAudioCodec.RAW; - break; - } - return { - type: "success", - codec, - stream: new PushReadableStream( - async (controller) => { - // Put the first 4 bytes back - await controller.enqueue(buffer); - - const stream = buffered.release(); - const reader = stream.getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - await controller.enqueue(value); - } - }, - ), - }; - })(); + }, + () => { + switch (this.value.audioCodec) { + case "raw": + return ScrcpyAudioCodec.RAW; + case "opus": + return ScrcpyAudioCodec.OPUS; + case "aac": + return ScrcpyAudioCodec.AAC; + } + }, + ); } override serializeInjectTouchControlMessage( diff --git a/libraries/scrcpy/src/options/2_3.ts b/libraries/scrcpy/src/options/2_3.ts index 6d6c2e5a..f684fdfe 100644 --- a/libraries/scrcpy/src/options/2_3.ts +++ b/libraries/scrcpy/src/options/2_3.ts @@ -1,11 +1,14 @@ +import type { ReadableStream } from "@yume-chan/stream-extra"; import { ScrcpyOptions1_21 } from "./1_21.js"; -import { omit } from "./2_0.js"; +import { ScrcpyOptions2_0, omit } from "./2_0.js"; import { ScrcpyOptions2_2, type ScrcpyOptionsInit2_2 } from "./2_2.js"; import { ScrcpyOptionsBase } from "./types.js"; +import type { ValueOrPromise } from "@yume-chan/struct"; +import { ScrcpyAudioCodec, type ScrcpyAudioStreamMetadata } from "./codec.js"; export interface ScrcpyOptionsInit2_3 extends Omit { - audioCodec?: "raw" | "opus" | "aac" | "flac" | undefined; + audioCodec?: "raw" | "opus" | "aac" | "flac"; } export class ScrcpyOptions2_3 extends ScrcpyOptionsBase< @@ -30,4 +33,41 @@ export class ScrcpyOptions2_3 extends ScrcpyOptionsBase< override serialize(): string[] { return ScrcpyOptions1_21.serialize(this.value, this.defaults); } + + override parseAudioStreamMetadata( + stream: ReadableStream, + ): ValueOrPromise { + 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; + } + }, + ); + } } diff --git a/libraries/stream-extra/src/concat.ts b/libraries/stream-extra/src/concat.ts index 2a40ed84..40650541 100644 --- a/libraries/stream-extra/src/concat.ts +++ b/libraries/stream-extra/src/concat.ts @@ -1,4 +1,5 @@ import { PromiseResolver } from "@yume-chan/async"; +import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct"; import type { ReadableStreamDefaultController } from "./stream.js"; import { ReadableStream, WritableStream } from "./stream.js"; @@ -83,7 +84,8 @@ export interface ConcatBufferReadableStream * If you want to decode the result as string, * prefer `.pipeThrough(new DecodeUtf8Stream()).pipeThrough(new ConcatStringStream())`, * than `.pipeThough(new ConcatBufferStream()).pipeThrough(new DecodeUtf8Stream())`, - * because concatenating strings is faster than concatenating `Uint8Array`s. + * because of JavaScript engine optimizations, + * concatenating strings is faster than concatenating `Uint8Array`s. */ export class ConcatBufferStream { #segments: Uint8Array[] = []; @@ -99,7 +101,7 @@ export class ConcatBufferStream { let offset = 0; switch (this.#segments.length) { case 0: - result = new Uint8Array(0); + result = EMPTY_UINT8_ARRAY; break; case 1: result = this.#segments[0]!; diff --git a/libraries/stream-extra/src/consumable.ts b/libraries/stream-extra/src/consumable.ts index 27e16679..e83f8b15 100644 --- a/libraries/stream-extra/src/consumable.ts +++ b/libraries/stream-extra/src/consumable.ts @@ -7,30 +7,9 @@ import type { } from "./stream.js"; import { WritableStream as NativeWritableStream, - ReadableStream, + ReadableStream as NativeReadableStream, } from "./stream.js"; - -interface Task { - run(callback: () => T): T; -} - -interface Console { - createTask(name: string): Task; -} - -interface GlobalExtension { - console: Console; -} - -// `createTask` allows browser DevTools to track the call stack across async boundaries. -const { console } = globalThis as unknown as GlobalExtension; -const createTask: Console["createTask"] = - console.createTask?.bind(console) ?? - (() => ({ - run(callback) { - return callback(); - }, - })); +import { createTask, type Task } from "./task.js"; function isPromiseLike(value: unknown): value is PromiseLike { return typeof value === "object" && value !== null && "then" in value; @@ -148,83 +127,78 @@ export namespace Consumable { ); } } -} -export interface ConsumableReadableStreamController { - enqueue(chunk: T): Promise; - close(): void; - error(reason: unknown): void; -} - -export interface ConsumableReadableStreamSource { - start?( - controller: ConsumableReadableStreamController, - ): void | PromiseLike; - pull?( - controller: ConsumableReadableStreamController, - ): void | PromiseLike; - cancel?(reason: unknown): void | PromiseLike; -} - -export class ConsumableReadableStream extends ReadableStream> { - static async enqueue( - controller: { enqueue: (chunk: Consumable) => void }, - chunk: T, - ) { - const output = new Consumable(chunk); - controller.enqueue(output); - await output.consumed; + export interface ReadableStreamController { + enqueue(chunk: T): Promise; + close(): void; + error(reason: unknown): void; } - constructor( - source: ConsumableReadableStreamSource, - strategy?: QueuingStrategy, - ) { - let wrappedController: - | ConsumableReadableStreamController - | undefined; + export interface ReadableStreamSource { + start?( + controller: ReadableStreamController, + ): void | PromiseLike; + pull?( + controller: ReadableStreamController, + ): void | PromiseLike; + cancel?(reason: unknown): void | PromiseLike; + } - let wrappedStrategy: QueuingStrategy> | undefined; - if (strategy) { - wrappedStrategy = {}; - if ("highWaterMark" in strategy) { - wrappedStrategy.highWaterMark = strategy.highWaterMark; - } - if ("size" in strategy) { - wrappedStrategy.size = (chunk) => { - return strategy.size!(chunk.value); - }; - } + export class ReadableStream extends NativeReadableStream> { + static async enqueue( + controller: { enqueue: (chunk: Consumable) => void }, + chunk: T, + ) { + const output = new Consumable(chunk); + controller.enqueue(output); + await output.consumed; } - super( - { - async start(controller) { - wrappedController = { - async enqueue(chunk) { - await ConsumableReadableStream.enqueue( - controller, - chunk, - ); - }, - close() { - controller.close(); - }, - error(reason) { - controller.error(reason); - }, - }; + constructor( + source: ReadableStreamSource, + strategy?: QueuingStrategy, + ) { + let wrappedController: ReadableStreamController | undefined; - await source.start?.(wrappedController); + let wrappedStrategy: QueuingStrategy> | undefined; + if (strategy) { + wrappedStrategy = {}; + if ("highWaterMark" in strategy) { + wrappedStrategy.highWaterMark = strategy.highWaterMark; + } + if ("size" in strategy) { + wrappedStrategy.size = (chunk) => { + return strategy.size!(chunk.value); + }; + } + } + + super( + { + async start(controller) { + wrappedController = { + async enqueue(chunk) { + await ReadableStream.enqueue(controller, chunk); + }, + close() { + controller.close(); + }, + error(reason) { + controller.error(reason); + }, + }; + + await source.start?.(wrappedController); + }, + async pull() { + await source.pull?.(wrappedController!); + }, + async cancel(reason) { + await source.cancel?.(reason); + }, }, - async pull() { - await source.pull?.(wrappedController!); - }, - async cancel(reason) { - await source.cancel?.(reason); - }, - }, - wrappedStrategy, - ); + wrappedStrategy, + ); + } } } diff --git a/libraries/stream-extra/src/distribution.spec.ts b/libraries/stream-extra/src/distribution.spec.ts index 0a6744ff..f7794ebc 100644 --- a/libraries/stream-extra/src/distribution.spec.ts +++ b/libraries/stream-extra/src/distribution.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { ConsumableReadableStream } from "./consumable.js"; +import { Consumable } from "./consumable.js"; import { DistributionStream } from "./distribution.js"; import { MaybeConsumable } from "./maybe-consumable.js"; @@ -17,7 +17,7 @@ async function testInputOutput( const write = jest.fn((chunk: Uint8Array) => { void chunk; }); - await new ConsumableReadableStream({ + await new Consumable.ReadableStream({ async start(controller) { let offset = 0; for (const length of inputLengths) { diff --git a/libraries/stream-extra/src/distribution.ts b/libraries/stream-extra/src/distribution.ts index fd2a99c4..293d05e2 100644 --- a/libraries/stream-extra/src/distribution.ts +++ b/libraries/stream-extra/src/distribution.ts @@ -1,4 +1,4 @@ -import { ConsumableReadableStream } from "./consumable.js"; +import { Consumable } from "./consumable.js"; import { MaybeConsumable } from "./maybe-consumable.js"; import { TransformStream } from "./stream.js"; @@ -90,7 +90,7 @@ export class DistributionStream extends TransformStream< await MaybeConsumable.tryConsume(chunk, async (chunk) => { if (combiner) { for (const buffer of combiner.push(chunk)) { - await ConsumableReadableStream.enqueue( + await Consumable.ReadableStream.enqueue( controller, buffer, ); @@ -100,7 +100,7 @@ export class DistributionStream extends TransformStream< let available = chunk.byteLength; while (available > 0) { const end = offset + size; - await ConsumableReadableStream.enqueue( + await Consumable.ReadableStream.enqueue( controller, chunk.subarray(offset, end), ); diff --git a/libraries/stream-extra/src/index.ts b/libraries/stream-extra/src/index.ts index 4493aa5e..9abc3361 100644 --- a/libraries/stream-extra/src/index.ts +++ b/libraries/stream-extra/src/index.ts @@ -13,5 +13,6 @@ export * from "./split-string.js"; export * from "./stream.js"; export * from "./struct-deserialize.js"; export * from "./struct-serialize.js"; +export * from "./task.js"; export * from "./wrap-readable.js"; export * from "./wrap-writable.js"; diff --git a/libraries/stream-extra/src/task.ts b/libraries/stream-extra/src/task.ts new file mode 100644 index 00000000..ebaf4fa6 --- /dev/null +++ b/libraries/stream-extra/src/task.ts @@ -0,0 +1,21 @@ +export interface Task { + run(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(); + }, + })); diff --git a/libraries/struct/src/struct.spec.ts b/libraries/struct/src/struct.spec.ts index 5d51b2fe..8e6d1823 100644 --- a/libraries/struct/src/struct.spec.ts +++ b/libraries/struct/src/struct.spec.ts @@ -13,11 +13,11 @@ import type { ValueOrPromise } from "./utils.js"; import { BigIntFieldDefinition, - BigIntFieldType, - BufferFieldSubType, + BigIntFieldVariant, + BufferFieldConverter, FixedLengthBufferLikeFieldDefinition, NumberFieldDefinition, - NumberFieldType, + NumberFieldVariant, VariableLengthBufferLikeFieldDefinition, } from "./index.js"; @@ -120,7 +120,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as NumberFieldDefinition; expect(definition).toBeInstanceOf(NumberFieldDefinition); - expect(definition.type).toBe(NumberFieldType.Int8); + expect(definition.variant).toBe(NumberFieldVariant.Int8); }); it("`uint8` should append an `uint8` field", () => { @@ -130,7 +130,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as NumberFieldDefinition; expect(definition).toBeInstanceOf(NumberFieldDefinition); - expect(definition.type).toBe(NumberFieldType.Uint8); + expect(definition.variant).toBe(NumberFieldVariant.Uint8); }); it("`int16` should append an `int16` field", () => { @@ -140,7 +140,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as NumberFieldDefinition; expect(definition).toBeInstanceOf(NumberFieldDefinition); - expect(definition.type).toBe(NumberFieldType.Int16); + expect(definition.variant).toBe(NumberFieldVariant.Int16); }); it("`uint16` should append an `uint16` field", () => { @@ -150,7 +150,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as NumberFieldDefinition; expect(definition).toBeInstanceOf(NumberFieldDefinition); - expect(definition.type).toBe(NumberFieldType.Uint16); + expect(definition.variant).toBe(NumberFieldVariant.Uint16); }); it("`int32` should append an `int32` field", () => { @@ -160,7 +160,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as NumberFieldDefinition; expect(definition).toBeInstanceOf(NumberFieldDefinition); - expect(definition.type).toBe(NumberFieldType.Int32); + expect(definition.variant).toBe(NumberFieldVariant.Int32); }); it("`uint32` should append an `uint32` field", () => { @@ -170,7 +170,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as NumberFieldDefinition; expect(definition).toBeInstanceOf(NumberFieldDefinition); - expect(definition.type).toBe(NumberFieldType.Uint32); + expect(definition.variant).toBe(NumberFieldVariant.Uint32); }); it("`int64` should append an `int64` field", () => { @@ -180,7 +180,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as BigIntFieldDefinition; expect(definition).toBeInstanceOf(BigIntFieldDefinition); - expect(definition.type).toBe(BigIntFieldType.Int64); + expect(definition.variant).toBe(BigIntFieldVariant.Int64); }); it("`uint64` should append an `uint64` field", () => { @@ -190,7 +190,7 @@ describe("Struct", () => { const definition = struct.fields[0]![1] as BigIntFieldDefinition; expect(definition).toBeInstanceOf(BigIntFieldDefinition); - expect(definition.type).toBe(BigIntFieldType.Uint64); + expect(definition.variant).toBe(BigIntFieldVariant.Uint64); }); describe("#uint8ArrayLike", () => { @@ -205,7 +205,9 @@ describe("Struct", () => { expect(definition).toBeInstanceOf( FixedLengthBufferLikeFieldDefinition, ); - expect(definition.type).toBeInstanceOf(BufferFieldSubType); + expect(definition.converter).toBeInstanceOf( + BufferFieldConverter, + ); expect(definition.options.length).toBe(10); }); @@ -219,7 +221,9 @@ describe("Struct", () => { expect(definition).toBeInstanceOf( FixedLengthBufferLikeFieldDefinition, ); - expect(definition.type).toBeInstanceOf(BufferFieldSubType); + expect(definition.converter).toBeInstanceOf( + BufferFieldConverter, + ); expect(definition.options.length).toBe(10); }); }); @@ -237,7 +241,9 @@ describe("Struct", () => { expect(definition).toBeInstanceOf( VariableLengthBufferLikeFieldDefinition, ); - expect(definition.type).toBeInstanceOf(BufferFieldSubType); + expect(definition.converter).toBeInstanceOf( + BufferFieldConverter, + ); expect(definition.options.lengthField).toBe("barLength"); }); @@ -253,7 +259,9 @@ describe("Struct", () => { expect(definition).toBeInstanceOf( VariableLengthBufferLikeFieldDefinition, ); - expect(definition.type).toBeInstanceOf(BufferFieldSubType); + expect(definition.converter).toBeInstanceOf( + BufferFieldConverter, + ); expect(definition.options.lengthField).toBe("barLength"); }); }); @@ -270,19 +278,31 @@ describe("Struct", () => { const field0 = struct.fields[0]!; expect(field0).toHaveProperty("0", "int8"); - expect(field0[1]).toHaveProperty("type", NumberFieldType.Int8); + expect(field0[1]).toHaveProperty( + "variant", + NumberFieldVariant.Int8, + ); const field1 = struct.fields[1]!; expect(field1).toHaveProperty("0", "int16"); - expect(field1[1]).toHaveProperty("type", NumberFieldType.Int16); + expect(field1[1]).toHaveProperty( + "variant", + NumberFieldVariant.Int16, + ); const field2 = struct.fields[2]!; expect(field2).toHaveProperty("0", "int32"); - expect(field2[1]).toHaveProperty("type", NumberFieldType.Int32); + expect(field2[1]).toHaveProperty( + "variant", + NumberFieldVariant.Int32, + ); const field3 = struct.fields[3]!; expect(field3).toHaveProperty("0", "int64"); - expect(field3[1]).toHaveProperty("type", BigIntFieldType.Int64); + expect(field3[1]).toHaveProperty( + "variant", + BigIntFieldVariant.Int64, + ); }); }); diff --git a/libraries/struct/src/struct.ts b/libraries/struct/src/struct.ts index dcdc7efb..58eb8f56 100644 --- a/libraries/struct/src/struct.ts +++ b/libraries/struct/src/struct.ts @@ -16,19 +16,19 @@ import { } from "./basic/index.js"; import { SyncPromise } from "./sync-promise.js"; import type { - BufferFieldSubType, + BufferFieldConverter, FixedLengthBufferLikeFieldOptions, LengthField, VariableLengthBufferLikeFieldOptions, } from "./types/index.js"; import { BigIntFieldDefinition, - BigIntFieldType, + BigIntFieldVariant, FixedLengthBufferLikeFieldDefinition, NumberFieldDefinition, - NumberFieldType, - StringBufferFieldSubType, - Uint8ArrayBufferFieldSubType, + NumberFieldVariant, + StringBufferFieldConverter, + Uint8ArrayBufferFieldConverter, VariableLengthBufferLikeFieldDefinition, } from "./types/index.js"; import type { Evaluate, Identity, Overwrite, ValueOrPromise } from "./utils.js"; @@ -86,7 +86,7 @@ interface ArrayBufferLikeFieldCreator< */ < TName extends PropertyKey, - TType extends BufferFieldSubType, + TType extends BufferFieldConverter, TTypeScriptType = TType["TTypeScriptType"], >( name: TName, @@ -110,7 +110,7 @@ interface ArrayBufferLikeFieldCreator< */ < TName extends PropertyKey, - TType extends BufferFieldSubType, + TType extends BufferFieldConverter, TOptions extends VariableLengthBufferLikeFieldOptions, TTypeScriptType = TType["TTypeScriptType"], >( @@ -136,7 +136,7 @@ interface BoundArrayBufferLikeFieldDefinitionCreator< TOmitInitKey extends PropertyKey, TExtra extends object, TPostDeserialized, - TType extends BufferFieldSubType, + TType extends BufferFieldConverter, > { ( name: TName, @@ -351,7 +351,7 @@ export class Struct< #number< TName extends PropertyKey, - TType extends NumberFieldType = NumberFieldType, + TType extends NumberFieldVariant = NumberFieldVariant, TTypeScriptType = number, >(name: TName, type: TType, typeScriptType?: TTypeScriptType) { return this.field( @@ -367,7 +367,7 @@ export class Struct< name: TName, typeScriptType?: TTypeScriptType, ) { - return this.#number(name, NumberFieldType.Int8, typeScriptType); + return this.#number(name, NumberFieldVariant.Int8, typeScriptType); } /** @@ -377,7 +377,7 @@ export class Struct< name: TName, typeScriptType?: TTypeScriptType, ) { - return this.#number(name, NumberFieldType.Uint8, typeScriptType); + return this.#number(name, NumberFieldVariant.Uint8, typeScriptType); } /** @@ -387,7 +387,7 @@ export class Struct< name: TName, typeScriptType?: TTypeScriptType, ) { - return this.#number(name, NumberFieldType.Int16, typeScriptType); + return this.#number(name, NumberFieldVariant.Int16, typeScriptType); } /** @@ -397,7 +397,7 @@ export class Struct< name: TName, typeScriptType?: TTypeScriptType, ) { - return this.#number(name, NumberFieldType.Uint16, typeScriptType); + return this.#number(name, NumberFieldVariant.Uint16, typeScriptType); } /** @@ -407,7 +407,7 @@ export class Struct< name: TName, typeScriptType?: TTypeScriptType, ) { - return this.#number(name, NumberFieldType.Int32, typeScriptType); + return this.#number(name, NumberFieldVariant.Int32, typeScriptType); } /** @@ -417,12 +417,12 @@ export class Struct< name: TName, typeScriptType?: TTypeScriptType, ) { - return this.#number(name, NumberFieldType.Uint32, typeScriptType); + return this.#number(name, NumberFieldVariant.Uint32, typeScriptType); } #bigint< TName extends PropertyKey, - TType extends BigIntFieldType = BigIntFieldType, + TType extends BigIntFieldVariant = BigIntFieldVariant, TTypeScriptType = TType["TTypeScriptType"], >(name: TName, type: TType, typeScriptType?: TTypeScriptType) { return this.field( @@ -438,9 +438,9 @@ export class Struct< */ int64< TName extends PropertyKey, - TTypeScriptType = BigIntFieldType["TTypeScriptType"], + TTypeScriptType = BigIntFieldVariant["TTypeScriptType"], >(name: TName, typeScriptType?: TTypeScriptType) { - return this.#bigint(name, BigIntFieldType.Int64, typeScriptType); + return this.#bigint(name, BigIntFieldVariant.Int64, typeScriptType); } /** @@ -450,9 +450,9 @@ export class Struct< */ uint64< TName extends PropertyKey, - TTypeScriptType = BigIntFieldType["TTypeScriptType"], + TTypeScriptType = BigIntFieldVariant["TTypeScriptType"], >(name: TName, typeScriptType?: TTypeScriptType) { - return this.#bigint(name, BigIntFieldType.Uint64, typeScriptType); + return this.#bigint(name, BigIntFieldVariant.Uint64, typeScriptType); } #arrayBufferLike: ArrayBufferLikeFieldCreator< @@ -462,7 +462,7 @@ export class Struct< TPostDeserialized > = ( name: PropertyKey, - type: BufferFieldSubType, + type: BufferFieldConverter, options: | FixedLengthBufferLikeFieldOptions | VariableLengthBufferLikeFieldOptions, @@ -485,7 +485,7 @@ export class Struct< TOmitInitKey, TExtra, TPostDeserialized, - Uint8ArrayBufferFieldSubType + Uint8ArrayBufferFieldConverter > = ( name: PropertyKey, options: unknown, @@ -493,7 +493,7 @@ export class Struct< ): never => { return this.#arrayBufferLike( name, - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, options as never, typeScriptType, ) as never; @@ -504,7 +504,7 @@ export class Struct< TOmitInitKey, TExtra, TPostDeserialized, - StringBufferFieldSubType + StringBufferFieldConverter > = ( name: PropertyKey, options: unknown, @@ -512,7 +512,7 @@ export class Struct< ): never => { return this.#arrayBufferLike( name, - StringBufferFieldSubType.Instance, + StringBufferFieldConverter.Instance, options as never, typeScriptType, ) as never; diff --git a/libraries/struct/src/types/bigint.ts b/libraries/struct/src/types/bigint.ts index 84e775f0..7355e717 100644 --- a/libraries/struct/src/types/bigint.ts +++ b/libraries/struct/src/types/bigint.ts @@ -14,53 +14,57 @@ import { StructFieldDefinition, StructFieldValue } from "../basic/index.js"; import { SyncPromise } from "../sync-promise.js"; import type { ValueOrPromise } from "../utils.js"; -type GetBigInt64 = ( +export type BigIntDeserializer = ( array: Uint8Array, byteOffset: number, littleEndian: boolean, ) => bigint; -type SetBigInt64 = ( +export type BigIntSerializer = ( array: Uint8Array, byteOffset: number, value: bigint, littleEndian: boolean, ) => void; -export class BigIntFieldType { +export class BigIntFieldVariant { readonly TTypeScriptType!: bigint; readonly size: number; - readonly getter: GetBigInt64; + readonly deserialize: BigIntDeserializer; - readonly setter: SetBigInt64; + readonly serialize: BigIntSerializer; - constructor(size: number, getter: GetBigInt64, setter: SetBigInt64) { + constructor( + size: number, + deserialize: BigIntDeserializer, + serialize: BigIntSerializer, + ) { this.size = size; - this.getter = getter; - this.setter = setter; + this.deserialize = deserialize; + this.serialize = serialize; } - static readonly Int64 = new BigIntFieldType(8, getInt64, setInt64); + static readonly Int64 = new BigIntFieldVariant(8, getInt64, setInt64); - static readonly Uint64 = new BigIntFieldType(8, getUint64, setUint64); + static readonly Uint64 = new BigIntFieldVariant(8, getUint64, setUint64); } export class BigIntFieldDefinition< - TType extends BigIntFieldType = BigIntFieldType, - TTypeScriptType = TType["TTypeScriptType"], + TVariant extends BigIntFieldVariant = BigIntFieldVariant, + TTypeScriptType = TVariant["TTypeScriptType"], > extends StructFieldDefinition { - readonly type: TType; + readonly variant: TVariant; - constructor(type: TType, typescriptType?: TTypeScriptType) { + constructor(variant: TVariant, typescriptType?: TTypeScriptType) { void typescriptType; super(); - this.type = type; + this.variant = variant; } getSize(): number { - return this.type.size; + return this.variant.size; } create( @@ -90,7 +94,11 @@ export class BigIntFieldDefinition< return stream.readExactly(this.getSize()); }) .then((array) => { - const value = this.type.getter(array, 0, options.littleEndian); + const value = this.variant.deserialize( + array, + 0, + options.littleEndian, + ); return this.create(options, struct, value as never); }) .valueOrPromise(); @@ -98,14 +106,14 @@ export class BigIntFieldDefinition< } export class BigIntFieldValue< - TDefinition extends BigIntFieldDefinition, + TDefinition extends BigIntFieldDefinition, > extends StructFieldValue { override serialize( dataView: DataView, array: Uint8Array, offset: number, ): void { - this.definition.type.setter( + this.definition.variant.serialize( array, offset, this.value as never, diff --git a/libraries/struct/src/types/buffer/base.spec.ts b/libraries/struct/src/types/buffer/base.spec.ts index 6d688aca..f3b56cfe 100644 --- a/libraries/struct/src/types/buffer/base.spec.ts +++ b/libraries/struct/src/types/buffer/base.spec.ts @@ -3,12 +3,12 @@ import { describe, expect, it, jest } from "@jest/globals"; import type { ExactReadable } from "../../basic/index.js"; import { StructDefaultOptions, StructValue } from "../../basic/index.js"; -import type { BufferFieldSubType } from "./base.js"; +import type { BufferFieldConverter } from "./base.js"; import { BufferLikeFieldDefinition, EMPTY_UINT8_ARRAY, - StringBufferFieldSubType, - Uint8ArrayBufferFieldSubType, + StringBufferFieldConverter, + Uint8ArrayBufferFieldConverter, } from "./base.js"; class MockDeserializationStream implements ExactReadable { @@ -23,37 +23,37 @@ describe("Types", () => { describe("Buffer", () => { describe("Uint8ArrayBufferFieldSubType", () => { it("should have a static instance", () => { - expect(Uint8ArrayBufferFieldSubType.Instance).toBeInstanceOf( - Uint8ArrayBufferFieldSubType, + expect(Uint8ArrayBufferFieldConverter.Instance).toBeInstanceOf( + Uint8ArrayBufferFieldConverter, ); }); it("`#toBuffer` should return the same `Uint8Array`", () => { const array = new Uint8Array(10); expect( - Uint8ArrayBufferFieldSubType.Instance.toBuffer(array), + Uint8ArrayBufferFieldConverter.Instance.toBuffer(array), ).toBe(array); }); it("`#fromBuffer` should return the same `Uint8Array`", () => { const buffer = new Uint8Array(10); expect( - Uint8ArrayBufferFieldSubType.Instance.toValue(buffer), + Uint8ArrayBufferFieldConverter.Instance.toValue(buffer), ).toBe(buffer); }); it("`#getSize` should return the `byteLength` of the `Uint8Array`", () => { const array = new Uint8Array(10); expect( - Uint8ArrayBufferFieldSubType.Instance.getSize(array), + Uint8ArrayBufferFieldConverter.Instance.getSize(array), ).toBe(10); }); }); describe("StringBufferFieldSubType", () => { it("should have a static instance", () => { - expect(StringBufferFieldSubType.Instance).toBeInstanceOf( - StringBufferFieldSubType, + expect(StringBufferFieldConverter.Instance).toBeInstanceOf( + StringBufferFieldConverter, ); }); @@ -61,25 +61,27 @@ describe("Types", () => { const text = "foo"; const array = new Uint8Array(Buffer.from(text, "utf-8")); expect( - StringBufferFieldSubType.Instance.toBuffer(text), + StringBufferFieldConverter.Instance.toBuffer(text), ).toEqual(array); }); it("`#fromBuffer` should return the encoded ArrayBuffer", () => { const text = "foo"; const array = new Uint8Array(Buffer.from(text, "utf-8")); - expect(StringBufferFieldSubType.Instance.toValue(array)).toBe( + expect(StringBufferFieldConverter.Instance.toValue(array)).toBe( text, ); }); it("`#getSize` should return -1", () => { - expect(StringBufferFieldSubType.Instance.getSize()).toBe(-1); + expect(StringBufferFieldConverter.Instance.getSize()).toBe( + undefined, + ); }); }); class MockArrayBufferFieldDefinition< - TType extends BufferFieldSubType, + TType extends BufferFieldConverter, > extends BufferLikeFieldDefinition { getSize(): number { return this.options; @@ -90,7 +92,7 @@ describe("Types", () => { it("should work with `Uint8ArrayBufferFieldSubType`", () => { const size = 10; const definition = new MockArrayBufferFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, size, ); @@ -114,7 +116,7 @@ describe("Types", () => { it("should work when `#getSize` returns `0`", () => { const size = 0; const definition = new MockArrayBufferFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, size, ); @@ -143,7 +145,7 @@ describe("Types", () => { it("should clear `array` field", () => { const size = 0; const definition = new MockArrayBufferFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, size, ); @@ -169,7 +171,7 @@ describe("Types", () => { it("should be able to serialize with cached `array`", () => { const size = 0; const definition = new MockArrayBufferFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, size, ); @@ -197,7 +199,7 @@ describe("Types", () => { it("should be able to serialize a modified value", () => { const size = 0; const definition = new MockArrayBufferFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, size, ); diff --git a/libraries/struct/src/types/buffer/base.ts b/libraries/struct/src/types/buffer/base.ts index d5d1549e..4bbdce32 100644 --- a/libraries/struct/src/types/buffer/base.ts +++ b/libraries/struct/src/types/buffer/base.ts @@ -11,46 +11,49 @@ import type { ValueOrPromise } from "../../utils.js"; import { decodeUtf8, encodeUtf8 } from "../../utils.js"; /** - * Base class for all types that - * can be converted from an `Uint8Array` when deserialized, - * and need to be converted to an `Uint8Array` when serializing + * A converter for buffer-like fields. + * It converts `Uint8Array`s to custom-typed values when deserializing, + * and convert values back to `Uint8Array`s when serializing. * - * @template TValue The actual TypeScript type of this type - * @template TTypeScriptType Optional another type (should be compatible with `TType`) - * specified by user when creating field definitions. + * @template TValue The type of the value that the converter converts to/from `Uint8Array`. + * @template TTypeScriptType Optionally another type to refine `TValue`. + * For example, `TValue` is `string`, and `TTypeScriptType` is `"foo" | "bar"`. + * `TValue` is specified by the developer when creating an converter implementation, + * `TTypeScriptType` is specified by the user when creating a field. */ -export abstract class BufferFieldSubType< +export abstract class BufferFieldConverter< TValue = unknown, TTypeScriptType = TValue, > { readonly TTypeScriptType!: TTypeScriptType; /** - * When implemented in derived classes, converts the type-specific `value` to an `Uint8Array` + * When implemented in derived classes, converts the custom `value` to an `Uint8Array` * * This function should be "pure", i.e., * same `value` should always be converted to `Uint8Array`s that have same content. */ abstract toBuffer(value: TValue): Uint8Array; - /** When implemented in derived classes, converts the `Uint8Array` to a type-specific value */ + /** When implemented in derived classes, converts the `Uint8Array` to a custom value */ abstract toValue(array: Uint8Array): TValue; /** - * When implemented in derived classes, gets the size in byte of the type-specific `value`. + * When implemented in derived classes, gets the size in byte of the custom `value`. * - * If the size can't be calculated without first converting the `value` back to an `Uint8Array`, - * implementer can returns `-1`, so the caller will get its size by first converting it to - * an `Uint8Array` (and cache the result). + * If the size can't be determined without first converting the `value` back to an `Uint8Array`, + * the implementer should return `undefined`. In which case, the caller will call `toBuffer` to + * convert the value to a `Uint8Array`, then read the length of the `Uint8Array`. The caller can + * cache the result so the serialization process doesn't need to call `toBuffer` again. */ - abstract getSize(value: TValue): number; + abstract getSize(value: TValue): number | undefined; } -/** An `BufferFieldSubType` that's actually an `Uint8Array` */ -export class Uint8ArrayBufferFieldSubType< +/** An identity converter, doesn't convert to anything else. */ +export class Uint8ArrayBufferFieldConverter< TTypeScriptType = Uint8Array, -> extends BufferFieldSubType { - static readonly Instance = new Uint8ArrayBufferFieldSubType(); +> extends BufferFieldConverter { + static readonly Instance = new Uint8ArrayBufferFieldConverter(); protected constructor() { super(); @@ -65,15 +68,15 @@ export class Uint8ArrayBufferFieldSubType< } getSize(value: Uint8Array): number { - return value.byteLength; + return value.length; } } /** An `BufferFieldSubType` that converts between `Uint8Array` and `string` */ -export class StringBufferFieldSubType< +export class StringBufferFieldConverter< TTypeScriptType = string, -> extends BufferFieldSubType { - static readonly Instance = new StringBufferFieldSubType(); +> extends BufferFieldConverter { + static readonly Instance = new StringBufferFieldConverter(); toBuffer(value: string): Uint8Array { return encodeUtf8(value); @@ -83,31 +86,29 @@ export class StringBufferFieldSubType< return decodeUtf8(array); } - getSize(): number { - // Return `-1`, so `BufferLikeFieldDefinition` will - // convert this `value` into an `Uint8Array` (and cache the result), - // Then get the size from that `Uint8Array` - return -1; + getSize(): number | undefined { + // See the note in `BufferFieldConverter.getSize` + return undefined; } } export const EMPTY_UINT8_ARRAY = new Uint8Array(0); export abstract class BufferLikeFieldDefinition< - TType extends BufferFieldSubType = BufferFieldSubType< + TConverter extends BufferFieldConverter< unknown, unknown - >, + > = BufferFieldConverter, TOptions = void, TOmitInitKey extends PropertyKey = never, - TTypeScriptType = TType["TTypeScriptType"], + TTypeScriptType = TConverter["TTypeScriptType"], > extends StructFieldDefinition { - readonly type: TType; + readonly converter: TConverter; readonly TTypeScriptType!: TTypeScriptType; - constructor(type: TType, options: TOptions) { + constructor(converter: TConverter, options: TOptions) { super(options); - this.type = type; + this.converter = converter; } protected getDeserializeSize(struct: StructValue): number { @@ -151,7 +152,7 @@ export abstract class BufferLikeFieldDefinition< } }) .then((array) => { - const value = this.type.toValue(array) as TTypeScriptType; + const value = this.converter.toValue(array) as TTypeScriptType; return this.create(options, struct, value, array); }) .valueOrPromise(); @@ -160,7 +161,7 @@ export abstract class BufferLikeFieldDefinition< export class BufferLikeFieldValue< TDefinition extends BufferLikeFieldDefinition< - BufferFieldSubType, + BufferFieldConverter, any, any, any @@ -191,7 +192,7 @@ export class BufferLikeFieldValue< array: Uint8Array, offset: number, ): void { - this.array ??= this.definition.type.toBuffer(this.value); + this.array ??= this.definition.converter.toBuffer(this.value); array.set(this.array, offset); } } diff --git a/libraries/struct/src/types/buffer/fixed-length.spec.ts b/libraries/struct/src/types/buffer/fixed-length.spec.ts index 98665d8b..7c26c354 100644 --- a/libraries/struct/src/types/buffer/fixed-length.spec.ts +++ b/libraries/struct/src/types/buffer/fixed-length.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@jest/globals"; -import { Uint8ArrayBufferFieldSubType } from "./base.js"; +import { Uint8ArrayBufferFieldConverter } from "./base.js"; import { FixedLengthBufferLikeFieldDefinition } from "./fixed-length.js"; describe("Types", () => { @@ -8,7 +8,7 @@ describe("Types", () => { describe("#getSize", () => { it("should return size in its options", () => { const definition = new FixedLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { length: 10 }, ); expect(definition.getSize()).toBe(10); diff --git a/libraries/struct/src/types/buffer/fixed-length.ts b/libraries/struct/src/types/buffer/fixed-length.ts index 7d89ec9b..f55f3561 100644 --- a/libraries/struct/src/types/buffer/fixed-length.ts +++ b/libraries/struct/src/types/buffer/fixed-length.ts @@ -1,4 +1,4 @@ -import type { BufferFieldSubType } from "./base.js"; +import type { BufferFieldConverter } from "./base.js"; import { BufferLikeFieldDefinition } from "./base.js"; export interface FixedLengthBufferLikeFieldOptions { @@ -6,11 +6,16 @@ export interface FixedLengthBufferLikeFieldOptions { } export class FixedLengthBufferLikeFieldDefinition< - TType extends BufferFieldSubType = BufferFieldSubType, + TConverter extends BufferFieldConverter = BufferFieldConverter, TOptions extends FixedLengthBufferLikeFieldOptions = FixedLengthBufferLikeFieldOptions, - TTypeScriptType = TType["TTypeScriptType"], -> extends BufferLikeFieldDefinition { + TTypeScriptType = TConverter["TTypeScriptType"], +> extends BufferLikeFieldDefinition< + TConverter, + TOptions, + never, + TTypeScriptType +> { getSize(): number { return this.options.length; } diff --git a/libraries/struct/src/types/buffer/variable-length.spec.ts b/libraries/struct/src/types/buffer/variable-length.spec.ts index 32ee5f9f..28f065ff 100644 --- a/libraries/struct/src/types/buffer/variable-length.spec.ts +++ b/libraries/struct/src/types/buffer/variable-length.spec.ts @@ -11,9 +11,9 @@ import { } from "../../basic/index.js"; import { - BufferFieldSubType, + BufferFieldConverter, EMPTY_UINT8_ARRAY, - Uint8ArrayBufferFieldSubType, + Uint8ArrayBufferFieldConverter, } from "./base.js"; import { VariableLengthBufferLikeFieldDefinition, @@ -334,7 +334,7 @@ describe("Types", () => { const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -371,7 +371,7 @@ describe("Types", () => { const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -409,7 +409,7 @@ describe("Types", () => { const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -433,7 +433,7 @@ describe("Types", () => { }); describe("#getSize", () => { - class MockArrayBufferFieldType extends BufferFieldSubType { + class MockArrayBufferFieldType extends BufferFieldConverter { override toBuffer = jest.fn((value: Uint8Array): Uint8Array => { return value; }); @@ -444,12 +444,14 @@ describe("Types", () => { }, ); - size = 0; + size: number | undefined = 0; - override getSize = jest.fn((value: Uint8Array): number => { - void value; - return this.size; - }); + override getSize = jest.fn( + (value: Uint8Array): number | undefined => { + void value; + return this.size; + }, + ); } it("should return cached size if exist", () => { @@ -546,7 +548,7 @@ describe("Types", () => { value, ); - arrayBufferFieldType.size = -1; + arrayBufferFieldType.size = undefined; expect(bufferFieldValue.getSize()).toBe(100); expect(arrayBufferFieldType.toValue).toHaveBeenCalledTimes(0); expect(arrayBufferFieldType.toBuffer).toHaveBeenCalledTimes(1); @@ -566,7 +568,7 @@ describe("Types", () => { const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -596,7 +598,7 @@ describe("Types", () => { const arrayBufferFieldDefinition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -622,7 +624,7 @@ describe("Types", () => { describe("#getSize", () => { it("should always return `0`", () => { const definition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField: "foo" }, ); expect(definition.getSize()).toBe(0); @@ -638,7 +640,7 @@ describe("Types", () => { struct.set(lengthField, originalLengthFieldValue); const definition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -660,7 +662,7 @@ describe("Types", () => { struct.set(lengthField, originalLengthFieldValue); const definition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -683,7 +685,7 @@ describe("Types", () => { const radix = 8; const definition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField, lengthFieldRadix: radix }, ); @@ -709,7 +711,7 @@ describe("Types", () => { struct.set(lengthField, originalLengthFieldValue); const definition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); @@ -742,7 +744,7 @@ describe("Types", () => { struct.set(lengthField, originalLengthFieldValue); const definition = new VariableLengthBufferLikeFieldDefinition( - Uint8ArrayBufferFieldSubType.Instance, + Uint8ArrayBufferFieldConverter.Instance, { lengthField }, ); diff --git a/libraries/struct/src/types/buffer/variable-length.ts b/libraries/struct/src/types/buffer/variable-length.ts index 85845720..e83f4bde 100644 --- a/libraries/struct/src/types/buffer/variable-length.ts +++ b/libraries/struct/src/types/buffer/variable-length.ts @@ -9,7 +9,7 @@ import type { import { StructFieldValue } from "../../basic/index.js"; import type { KeysOfType } from "../../utils.js"; -import type { BufferFieldSubType } from "./base.js"; +import type { BufferFieldConverter } from "./base.js"; import { BufferLikeFieldDefinition, BufferLikeFieldValue } from "./base.js"; export type LengthField = KeysOfType; @@ -35,12 +35,12 @@ export interface VariableLengthBufferLikeFieldOptions< } export class VariableLengthBufferLikeFieldDefinition< - TType extends BufferFieldSubType = BufferFieldSubType, + TConverter extends BufferFieldConverter = BufferFieldConverter, TOptions extends VariableLengthBufferLikeFieldOptions = VariableLengthBufferLikeFieldOptions, - TTypeScriptType = TType["TTypeScriptType"], + TTypeScriptType = TConverter["TTypeScriptType"], > extends BufferLikeFieldDefinition< - TType, + TConverter, TOptions, TOptions["lengthField"], TTypeScriptType @@ -106,14 +106,24 @@ export class VariableLengthBufferLikeStructFieldValue< } override getSize() { - if (this.length === undefined) { - this.length = this.definition.type.getSize(this.value); - if (this.length === -1) { - this.array = this.definition.type.toBuffer(this.value); - this.length = this.array.byteLength; - } + if (this.length !== undefined) { + // Have cached length + return this.length; } + this.length = this.definition.converter.getSize(this.value); + if (this.length !== undefined) { + if (this.length < 0) { + throw new Error("Invalid length"); + } + + // The converter knows the size + return this.length; + } + + // The converter doesn't know the size, so we need to convert to buffer first + this.array = this.definition.converter.toBuffer(this.value); + this.length = this.array.byteLength; return this.length; } diff --git a/libraries/struct/src/types/number.spec.ts b/libraries/struct/src/types/number.spec.ts index 95519c77..951ad285 100644 --- a/libraries/struct/src/types/number.spec.ts +++ b/libraries/struct/src/types/number.spec.ts @@ -3,10 +3,10 @@ import { describe, expect, it, jest, test } from "@jest/globals"; import type { ExactReadable } from "../basic/index.js"; import { StructDefaultOptions, StructValue } from "../basic/index.js"; -import { NumberFieldDefinition, NumberFieldType } from "./number.js"; +import { NumberFieldDefinition, NumberFieldVariant } from "./number.js"; function testEndian( - type: NumberFieldType, + type: NumberFieldVariant, min: number, max: number, littleEndian: boolean, @@ -55,7 +55,7 @@ function testEndian( }); } -function testDeserialize(type: NumberFieldType) { +function testDeserialize(type: NumberFieldVariant) { if (type.size === 1) { if (type.signed) { const MIN = -(2 ** (type.size * 8 - 1)); @@ -89,65 +89,65 @@ function testDeserialize(type: NumberFieldType) { describe("Types", () => { describe("Number", () => { - describe("NumberFieldType", () => { + describe("NumberFieldVariant", () => { describe("Int8", () => { const key = "Int8"; test("basic", () => { - expect(NumberFieldType[key]).toHaveProperty("size", 1); + expect(NumberFieldVariant[key]).toHaveProperty("size", 1); }); - testDeserialize(NumberFieldType[key]); + testDeserialize(NumberFieldVariant[key]); }); describe("Uint8", () => { const key = "Uint8"; test("basic", () => { - expect(NumberFieldType[key]).toHaveProperty("size", 1); + expect(NumberFieldVariant[key]).toHaveProperty("size", 1); }); - testDeserialize(NumberFieldType[key]); + testDeserialize(NumberFieldVariant[key]); }); describe("Int16", () => { const key = "Int16"; test("basic", () => { - expect(NumberFieldType[key]).toHaveProperty("size", 2); + expect(NumberFieldVariant[key]).toHaveProperty("size", 2); }); - testDeserialize(NumberFieldType[key]); + testDeserialize(NumberFieldVariant[key]); }); describe("Uint16", () => { const key = "Uint16"; test("basic", () => { - expect(NumberFieldType[key]).toHaveProperty("size", 2); + expect(NumberFieldVariant[key]).toHaveProperty("size", 2); }); - testDeserialize(NumberFieldType[key]); + testDeserialize(NumberFieldVariant[key]); }); describe("Int32", () => { const key = "Int32"; test("basic", () => { - expect(NumberFieldType[key]).toHaveProperty("size", 4); + expect(NumberFieldVariant[key]).toHaveProperty("size", 4); }); - testDeserialize(NumberFieldType[key]); + testDeserialize(NumberFieldVariant[key]); }); describe("Uint32", () => { const key = "Uint32"; test("basic", () => { - expect(NumberFieldType[key]).toHaveProperty("size", 4); + expect(NumberFieldVariant[key]).toHaveProperty("size", 4); }); - testDeserialize(NumberFieldType[key]); + testDeserialize(NumberFieldVariant[key]); }); }); @@ -156,32 +156,32 @@ describe("Types", () => { it("should return size of its type", () => { expect( new NumberFieldDefinition( - NumberFieldType.Int8, + NumberFieldVariant.Int8, ).getSize(), ).toBe(1); expect( new NumberFieldDefinition( - NumberFieldType.Uint8, + NumberFieldVariant.Uint8, ).getSize(), ).toBe(1); expect( new NumberFieldDefinition( - NumberFieldType.Int16, + NumberFieldVariant.Int16, ).getSize(), ).toBe(2); expect( new NumberFieldDefinition( - NumberFieldType.Uint16, + NumberFieldVariant.Uint16, ).getSize(), ).toBe(2); expect( new NumberFieldDefinition( - NumberFieldType.Int32, + NumberFieldVariant.Int32, ).getSize(), ).toBe(4); expect( new NumberFieldDefinition( - NumberFieldType.Uint32, + NumberFieldVariant.Uint32, ).getSize(), ).toBe(4); }); @@ -195,7 +195,7 @@ describe("Types", () => { const stream: ExactReadable = { position: 0, readExactly }; const definition = new NumberFieldDefinition( - NumberFieldType.Uint8, + NumberFieldVariant.Uint8, ); const struct = new StructValue({}); const value = definition.deserialize( @@ -207,7 +207,7 @@ describe("Types", () => { expect(value.get()).toBe(1); expect(readExactly).toHaveBeenCalledTimes(1); expect(readExactly).toHaveBeenCalledWith( - NumberFieldType.Uint8.size, + NumberFieldVariant.Uint8.size, ); }); @@ -218,7 +218,7 @@ describe("Types", () => { const stream: ExactReadable = { position: 0, readExactly }; const definition = new NumberFieldDefinition( - NumberFieldType.Uint16, + NumberFieldVariant.Uint16, ); const struct = new StructValue({}); const value = definition.deserialize( @@ -230,7 +230,7 @@ describe("Types", () => { expect(value.get()).toBe((1 << 8) | 2); expect(readExactly).toHaveBeenCalledTimes(1); expect(readExactly).toHaveBeenCalledWith( - NumberFieldType.Uint16.size, + NumberFieldVariant.Uint16.size, ); }); @@ -241,7 +241,7 @@ describe("Types", () => { const stream: ExactReadable = { position: 0, readExactly }; const definition = new NumberFieldDefinition( - NumberFieldType.Uint16, + NumberFieldVariant.Uint16, ); const struct = new StructValue({}); const value = definition.deserialize( @@ -253,7 +253,7 @@ describe("Types", () => { expect(value.get()).toBe((2 << 8) | 1); expect(readExactly).toHaveBeenCalledTimes(1); expect(readExactly).toHaveBeenCalledWith( - NumberFieldType.Uint16.size, + NumberFieldVariant.Uint16.size, ); }); }); @@ -265,37 +265,37 @@ describe("Types", () => { const struct = new StructValue({}); expect( - new NumberFieldDefinition(NumberFieldType.Int8) + new NumberFieldDefinition(NumberFieldVariant.Int8) .create(StructDefaultOptions, struct, 42) .getSize(), ).toBe(1); expect( - new NumberFieldDefinition(NumberFieldType.Uint8) + new NumberFieldDefinition(NumberFieldVariant.Uint8) .create(StructDefaultOptions, struct, 42) .getSize(), ).toBe(1); expect( - new NumberFieldDefinition(NumberFieldType.Int16) + new NumberFieldDefinition(NumberFieldVariant.Int16) .create(StructDefaultOptions, struct, 42) .getSize(), ).toBe(2); expect( - new NumberFieldDefinition(NumberFieldType.Uint16) + new NumberFieldDefinition(NumberFieldVariant.Uint16) .create(StructDefaultOptions, struct, 42) .getSize(), ).toBe(2); expect( - new NumberFieldDefinition(NumberFieldType.Int32) + new NumberFieldDefinition(NumberFieldVariant.Int32) .create(StructDefaultOptions, struct, 42) .getSize(), ).toBe(4); expect( - new NumberFieldDefinition(NumberFieldType.Uint32) + new NumberFieldDefinition(NumberFieldVariant.Uint32) .create(StructDefaultOptions, struct, 42) .getSize(), ).toBe(4); @@ -305,7 +305,7 @@ describe("Types", () => { describe("#serialize", () => { it("should serialize uint8", () => { const definition = new NumberFieldDefinition( - NumberFieldType.Int8, + NumberFieldVariant.Int8, ); const struct = new StructValue({}); const value = definition.create( diff --git a/libraries/struct/src/types/number.ts b/libraries/struct/src/types/number.ts index f3be9e54..f104bc97 100644 --- a/libraries/struct/src/types/number.ts +++ b/libraries/struct/src/types/number.ts @@ -15,7 +15,7 @@ import { StructFieldDefinition, StructFieldValue } from "../basic/index.js"; import { SyncPromise } from "../sync-promise.js"; import type { ValueOrPromise } from "../utils.js"; -export interface NumberFieldType { +export interface NumberFieldVariant { signed: boolean; size: number; deserialize(array: Uint8Array, littleEndian: boolean): number; @@ -27,8 +27,8 @@ export interface NumberFieldType { ): void; } -export namespace NumberFieldType { - export const Uint8: NumberFieldType = { +export namespace NumberFieldVariant { + export const Uint8: NumberFieldVariant = { signed: false, size: 1, deserialize(array) { @@ -39,7 +39,7 @@ export namespace NumberFieldType { }, }; - export const Int8: NumberFieldType = { + export const Int8: NumberFieldVariant = { signed: true, size: 1, deserialize(array) { @@ -51,7 +51,7 @@ export namespace NumberFieldType { }, }; - export const Uint16: NumberFieldType = { + export const Uint16: NumberFieldVariant = { signed: false, size: 2, deserialize(array, littleEndian) { @@ -65,7 +65,7 @@ export namespace NumberFieldType { }, }; - export const Int16: NumberFieldType = { + export const Int16: NumberFieldVariant = { signed: true, size: 2, deserialize(array, littleEndian) { @@ -76,7 +76,7 @@ export namespace NumberFieldType { }, }; - export const Uint32: NumberFieldType = { + export const Uint32: NumberFieldVariant = { signed: false, size: 4, deserialize(array, littleEndian) { @@ -87,7 +87,7 @@ export namespace NumberFieldType { }, }; - export const Int32: NumberFieldType = { + export const Int32: NumberFieldVariant = { signed: true, size: 4, deserialize(array, littleEndian) { @@ -100,19 +100,19 @@ export namespace NumberFieldType { } export class NumberFieldDefinition< - TType extends NumberFieldType = NumberFieldType, + TVariant extends NumberFieldVariant = NumberFieldVariant, TTypeScriptType = number, > extends StructFieldDefinition { - readonly type: TType; + readonly variant: TVariant; - constructor(type: TType, typescriptType?: TTypeScriptType) { + constructor(variant: TVariant, typescriptType?: TTypeScriptType) { void typescriptType; super(); - this.type = type; + this.variant = variant; } getSize(): number { - return this.type.size; + return this.variant.size; } create( @@ -142,7 +142,7 @@ export class NumberFieldDefinition< return stream.readExactly(this.getSize()); }) .then((array) => { - const value = this.type.deserialize( + const value = this.variant.deserialize( array, options.littleEndian, ); @@ -153,10 +153,10 @@ export class NumberFieldDefinition< } export class NumberFieldValue< - TDefinition extends NumberFieldDefinition, + TDefinition extends NumberFieldDefinition, > extends StructFieldValue { serialize(dataView: DataView, array: Uint8Array, offset: number): void { - this.definition.type.serialize( + this.definition.variant.serialize( dataView, offset, this.value as never, diff --git a/libraries/struct/src/utils.ts b/libraries/struct/src/utils.ts index c78fbac5..866ab30c 100644 --- a/libraries/struct/src/utils.ts +++ b/libraries/struct/src/utils.ts @@ -88,5 +88,7 @@ export function encodeUtf8(input: string): Uint8Array { } export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string { + // `TextDecoder` has internal states in stream mode, + // but we don't use stream mode here, so it's safe to reuse the instance return Utf8Decoder.decode(buffer); } diff --git a/toolchain/eslint-config/package.json b/toolchain/eslint-config/package.json index dc9546e2..38b574e0 100644 --- a/toolchain/eslint-config/package.json +++ b/toolchain/eslint-config/package.json @@ -7,9 +7,9 @@ "run-eslint": "run-eslint.js" }, "dependencies": { - "@eslint/js": "^9.1.1", - "@types/node": "^20.12.7", - "eslint": "^9.1.1", + "@eslint/js": "^9.2.0", + "@types/node": "^20.12.8", + "eslint": "^9.2.0", "typescript": "^5.4.5", "typescript-eslint": "^7.8.0" },