├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── fuzz.yml ├── .gitignore ├── .mocharc.js ├── .nycrc.json ├── .travis.yml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── .gitignore ├── benchmark-from-msgpack-lite-data.json ├── benchmark-from-msgpack-lite.ts ├── decode-string.ts ├── encode-string.ts ├── key-decoder.ts ├── msgpack-benchmark.js ├── package.json ├── profile-decode.ts ├── profile-encode.ts ├── sample-large.json ├── string.ts ├── sync-vs-async.ts └── timestamp-ext.ts ├── codecov.yml ├── eslint.config.mjs ├── example ├── deno-with-esmsh.ts ├── deno-with-jsdeliver.ts ├── deno-with-npm.ts ├── deno-with-unpkg.ts ├── fetch-example-server.ts ├── fetch-example.html ├── umd-example.html ├── umd-example.js └── webpack-example │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.ts ├── karma.conf.ts ├── mod.ts ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── CachedKeyDecoder.ts ├── DecodeError.ts ├── Decoder.ts ├── Encoder.ts ├── ExtData.ts ├── ExtensionCodec.ts ├── context.ts ├── decode.ts ├── decodeAsync.ts ├── encode.ts ├── index.ts ├── timestamp.ts └── utils │ ├── int.ts │ ├── prettyByte.ts │ ├── stream.ts │ ├── typedArrays.ts │ └── utf8.ts ├── test ├── CachedKeyDecoder.test.ts ├── ExtensionCodec.test.ts ├── bigint64.test.ts ├── bun.spec.ts ├── codec-bigint.test.ts ├── codec-float.test.ts ├── codec-int.test.ts ├── codec-timestamp.test.ts ├── decode-blob.test.ts ├── decode-max-length.test.ts ├── decode-raw-strings.test.ts ├── decode.jsfuzz.js ├── decodeArrayStream.test.ts ├── decodeAsync.test.ts ├── decodeMulti.test.ts ├── decodeMultiStream.test.ts ├── deno_cjs_test.ts ├── deno_test.ts ├── edge-cases.test.ts ├── encode.test.ts ├── karma-run.ts ├── msgpack-ext.test.ts ├── msgpack-test-suite.test.ts ├── prototype-pollution.test.ts ├── readme.test.ts ├── reuse-instances-with-extensions.test.ts ├── reuse-instances.test.ts └── whatwg-streams.test.ts ├── tools ├── fix-ext.mts └── get-release-tag.mjs ├── tsconfig.dist.cjs.json ├── tsconfig.dist.esm.json ├── tsconfig.dist.webpack.json ├── tsconfig.json ├── tsconfig.test-karma.json └── webpack.config.mjs /.gitattributes: -------------------------------------------------------------------------------- 1 | .vscode/*.json linguist-language=jsonc 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | nodejs: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: 16 | - '18' 17 | - '20' 18 | - '22' 19 | - '24' 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Setup Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | cache: npm 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install -g nyc 28 | - run: npm ci 29 | - run: npm run test:cover 30 | - uses: codecov/codecov-action@v5 31 | with: 32 | files: coverage/coverage-final.json 33 | token: ${{ secrets.CODECOV_TOKEN }} 34 | 35 | browser: 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | browser: [ChromeHeadless, FirefoxHeadless] 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Setup Node.js 43 | uses: actions/setup-node@v4 44 | with: 45 | cache: npm 46 | node-version: '22' 47 | - run: npm install -g npm 48 | - run: npm ci 49 | - run: npm run test:browser -- --browsers ${{ matrix.browser }} 50 | 51 | lint: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - name: Setup Node.js 56 | uses: actions/setup-node@v4 57 | with: 58 | cache: npm 59 | node-version: '22' 60 | - run: npm ci 61 | - run: npx tsc 62 | - run: npm run lint 63 | 64 | deno: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | - name: Setup Deno 69 | uses: denoland/setup-deno@v2 70 | with: 71 | deno-version: "v2.x" 72 | - run: npm ci 73 | - run: npm run test:deno 74 | 75 | bun: 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: actions/checkout@v4 79 | - name: Setup Bun 80 | uses: oven-sh/setup-bun@v2 81 | - run: bun install 82 | - run: npm run test:bun 83 | 84 | node_with_strip_types: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v4 88 | - name: Setup Node.js 89 | uses: actions/setup-node@v4 90 | with: 91 | cache: npm 92 | node-version: '24' 93 | - run: npm ci 94 | - run: npm run test:node_with_strip_types 95 | 96 | timeline: 97 | runs-on: ubuntu-latest 98 | permissions: 99 | actions: read 100 | needs: 101 | - nodejs 102 | - browser 103 | - lint 104 | - deno 105 | - bun 106 | steps: 107 | - uses: Kesin11/actions-timeline@v2 108 | 109 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: '44 6 * * 6' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v3 26 | with: 27 | languages: typescript 28 | 29 | - name: Perform CodeQL Analysis 30 | uses: github/codeql-action/analyze@v3 31 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yml: -------------------------------------------------------------------------------- 1 | # https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz 2 | 3 | name: Fuzz 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | fuzzing: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | cache: npm 22 | node-version: "20" 23 | 24 | # npm@9 may fail with https://github.com/npm/cli/issues/6723 25 | # npm@10 may fail with "GitFetcher requires an Arborist constructor to pack a tarball" 26 | - run: npm install -g npm@8 27 | - run: npm ci 28 | - run: npm run test:fuzz 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | dist.*/ 4 | build/ 5 | .nyc_output/ 6 | coverage/ 7 | benchmark/sandbox.ts 8 | 9 | # v8 profiler logs 10 | isolate-*.log 11 | 12 | # tsimp 13 | .tsimp/ 14 | 15 | # deno 16 | deno.lock 17 | 18 | # flamebearer 19 | flamegraph.html 20 | 21 | # jsfuzz 22 | corpus/ 23 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require("ts-node/register"); 4 | 5 | module.exports = { 6 | diff: true, 7 | extension: ['ts'], 8 | package: '../package.json', 9 | timeout: 10000, 10 | }; 11 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "src/**/*.mts"], 3 | "extension": [".ts", ".mtx"], 4 | "reporter": [], 5 | "sourceMap": true, 6 | "instrument": true 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | firefox: latest 4 | env: 5 | global: 6 | # SAUCE_USERNAME 7 | - secure: J+FOPE/vVK6yzVXHVE7xibFV/hV+Ehc78MBADLlE10YIY7Ag6JkVeomgqRFB9I8zFzj5DALkpzOLGx4iIrFs6iYiNnEcl39fkm8myHl8xIuW+KHt5QOsCtM5qmvfSEZhJV+La0lSzFicjY9VX90VLZvJOHIbiCvIFRoxnwYVw6o= 8 | # SAUCE_ACCESS_KEY 9 | - secure: ay3CSAjya+UQDi0RulLIl6q25oobwLsjLbdkeASgjBq0qN5dXgFgEpBjecBxFqPGrwzzCj9K9fR81NWV80EjLkGdcfN0oGx0wvsOo2C2ulWGHc1dRgKUnMKAA2TL3br14KMfmGn6fmr+fA7Vq+qWajQpExlG0Kuw68C9iNuKIQw= 10 | matrix: 11 | include: 12 | - node_js: 10 13 | - node_js: 12 14 | - node_js: 14 15 | - node_js: lts/* 16 | env: BROWSER=FirefoxHeadless 17 | - node_js: lts/* 18 | env: BROWSER=slChrome 19 | - node_js: lts/* 20 | env: BROWSER=slFirefox 21 | - node_js: lts/* 22 | env: BROWSER=slSafari 23 | - node_js: lts/* 24 | env: BROWSER=slIE 25 | - node_js: lts/* 26 | env: BROWSER=slEdge 27 | - node_js: lts/* 28 | env: BROWSER=slIos 29 | - node_js: lts/* 30 | env: BROWSER=slAndroid 31 | fast_finish: true 32 | allow_failures: 33 | # Because Travis CI does not expose credentials to pull-request builds from forked repositories. 34 | # https://docs.travis-ci.com/user/pull-requests/#pull-requests-and-security-restrictions 35 | - env: BROWSER=slChrome 36 | - env: BROWSER=slFirefox 37 | - env: BROWSER=slSafari 38 | - env: BROWSER=slIE 39 | - env: BROWSER=slEdge 40 | - env: BROWSER=slIos 41 | - env: BROWSER=slAndroid 42 | cache: npm 43 | install: | 44 | npm install -g npm 45 | if [ "${BROWSER}" = "" ] 46 | then npm install -g nyc codecov 47 | fi 48 | npm ci 49 | script: | 50 | if [ "${BROWSER}" = "" ] 51 | then npm run test:cover 52 | else 53 | travis_wait 600 npm run test:browser -- --browsers "$BROWSER" 54 | fi 55 | after_success: | 56 | if [ "${BROWSER}" = "" ] 57 | then codecov -f coverage/*.json 58 | fi 59 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions which should be recommended for users of this workspace. 3 | "recommendations": [ 4 | "dbaeumer.vscode-eslint", 5 | "yzhang.markdown-all-in-one" 6 | ], 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [ 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // For configurations: 2 | // https://code.visualstudio.com/Docs/editor/debugging 3 | { 4 | "version": "0.2.0", 5 | "configurations": [ 6 | { 7 | "name": "Run the current Mocha test file", 8 | "type": "node", 9 | "sourceMaps": true, 10 | "request": "launch", 11 | "internalConsoleOptions": "openOnSessionStart", 12 | "runtimeExecutable": "npx", 13 | "program": "mocha", 14 | "args": [ 15 | "--colors", 16 | "${relativeFile}" 17 | ], 18 | "cwd": "${workspaceFolder}" 19 | }, 20 | { 21 | "name": "Run the current TypeScript file", 22 | "type": "node", 23 | "sourceMaps": true, 24 | "request": "launch", 25 | "internalConsoleOptions": "openOnSessionStart", 26 | "args": [ 27 | "--nolazy", 28 | "-r", 29 | "ts-node/register", 30 | "${relativeFile}" 31 | ], 32 | "cwd": "${workspaceFolder}" 33 | }, 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.eol": "\n", 4 | "editor.tabSize": 2, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit" 7 | }, 8 | "cSpell.words": [ 9 | "instanceof", 10 | "tsdoc", 11 | "typeof", 12 | "whatwg" 13 | ], 14 | "makefile.configureOnOpen": false 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # This is the revision history of @msgpack/msgpack 2 | 3 | ## 3.1.2 2025-05-25 4 | 5 | https://github.com/msgpack/msgpack-javascript/compare/v3.1.1...v3.1.2 6 | 7 | * Make sure this library works with `node --experimental-strip-types` 8 | 9 | ## 3.1.1 2025-03-12 10 | 11 | https://github.com/msgpack/msgpack-javascript/compare/v3.1.0...v3.1.1 12 | 13 | * Stop using `Symbol.dispose`, which is not yet supported in some environments ([#268](https://github.com/msgpack/msgpack-javascript/pull/268) by @rijenkii) 14 | 15 | 16 | ## 3.1.0 2025-02-21 17 | 18 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.1...v3.1.0 19 | 20 | * Added support for nonstandard map keys in the decoder ([#266](https://github.com/msgpack/msgpack-javascript/pull/266) by @PejmanNik) 21 | 22 | ## 3.0.1 2025-02-11 23 | 24 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.0...v3.0.1 25 | 26 | * Implement a tiny polyfill to Symbol.dispose ([#261](https://github.com/msgpack/msgpack-javascript/pull/261) to fix #260) 27 | 28 | 29 | ## 3.0.0 2025-02-07 30 | 31 | https://github.com/msgpack/msgpack-javascript/compare/v2.8.0...v3.0.0 32 | 33 | * Set the compile target to ES2020, dropping support for the dists with the ES5 target 34 | * Fixed a bug that `encode()` and `decode()` were not re-entrant in reusing instances ([#257](https://github.com/msgpack/msgpack-javascript/pull/257)) 35 | * Allowed the data alignment to support zero-copy decoding ([#248](https://github.com/msgpack/msgpack-javascript/pull/248), thanks to @EddiG) 36 | * Added an option `rawStrings: boolean` to decoders ([#235](https://github.com/msgpack/msgpack-javascript/pull/235), thanks to @jasonpaulos) 37 | * Optimized GC load by reusing stack states ([#228](https://github.com/msgpack/msgpack-javascript/pull/228), thanks to @sergeyzenchenko) 38 | * Added an option `useBigInt64` to map JavaScript's BigInt to MessagePack's int64 and uint64 ([#223](https://github.com/msgpack/msgpack-javascript/pull/223)) 39 | * Drop IE11 support ([#221](https://github.com/msgpack/msgpack-javascript/pull/221)) 40 | * It also fixes [feature request: option to disable TEXT_ENCODING env check #219](https://github.com/msgpack/msgpack-javascript/issues/219) 41 | * Change the interfaces of `Encoder` and `Decoder`, and describe the interfaces in README.md ([#224](https://github.com/msgpack/msgpack-javascript/pull/224)): 42 | * `new Encoder(options: EncoderOptions)`: it takes the same named-options as `encode()` 43 | * `new Decoder(options: DecoderOptions)`: it takes the same named-options as `decode()` 44 | 45 | ## 3.0.0-beta6 2025-02-07 46 | 47 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.0-beta5...v3.0.0-beta6 48 | 49 | * Set the compile target to ES2020, dropping support for the dists with the ES5 target 50 | 51 | ## 3.0.0-beta5 2025-02-06 52 | 53 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.0-beta4...v3.0.0-beta5 54 | 55 | * Fixed a bug that `encode()` and `decode()` were not re-entrant in reusing instances ([#257](https://github.com/msgpack/msgpack-javascript/pull/257)) 56 | 57 | ## 3.0.0-beta4 2025-02-04 58 | 59 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.0-beta3...v3.0.0-beta4 60 | 61 | * Added Deno test to CI 62 | * Added Bun tests to CI 63 | * Allowed the data alignment to support zero-copy decoding ([#248](https://github.com/msgpack/msgpack-javascript/pull/248), thanks to @EddiG) 64 | 65 | ## 3.0.0-beta3 2025-01-26 66 | 67 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.0-beta2...v3.0.0-beta3 68 | 69 | * Added an option `rawStrings: boolean` to decoders ([#235](https://github.com/msgpack/msgpack-javascript/pull/235), thanks to @jasonpaulos) 70 | * Optimized GC load by reusing stack states ([#228](https://github.com/msgpack/msgpack-javascript/pull/228), thanks to @sergeyzenchenko) 71 | * Drop support for Node.js v16 72 | * Type compatibility with ES2024 / SharedArrayBuffer 73 | 74 | ## 3.0.0-beta2 75 | 76 | https://github.com/msgpack/msgpack-javascript/compare/v3.0.0-beta1...v3.0.0-beta2 77 | 78 | * Upgrade TypeScript compiler to v5.0 79 | 80 | ## 3.0.0-beta1 81 | 82 | https://github.com/msgpack/msgpack-javascript/compare/v2.8.0...v3.0.0-beta1 83 | 84 | * Added an option `useBigInt64` to map JavaScript's BigInt to MessagePack's int64 and uint64 ([#223](https://github.com/msgpack/msgpack-javascript/pull/223)) 85 | * Drop IE11 support ([#221](https://github.com/msgpack/msgpack-javascript/pull/221)) 86 | * It also fixes [feature request: option to disable TEXT_ENCODING env check #219](https://github.com/msgpack/msgpack-javascript/issues/219) 87 | * Change the interfaces of `Encoder` and `Decoder`, and describe the interfaces in README.md ([#224](https://github.com/msgpack/msgpack-javascript/pull/224)): 88 | * `new Encoder(options: EncoderOptions)`: it takes the same named-options as `encode()` 89 | * `new Decoder(options: DecoderOptions)`: it takes the same named-options as `decode()` 90 | 91 | ## 2.8.0 2022-09-02 92 | 93 | https://github.com/msgpack/msgpack-javascript/compare/v2.7.2...v2.8.0 94 | 95 | * Let `Encoder#encode()` return a copy of the internal buffer, instead of the reference of the buffer (fix #212). 96 | * Introducing `Encoder#encodeSharedRef()` to return the shared reference to the internal buffer. 97 | 98 | ## 2.7.2 2022/02/08 99 | 100 | https://github.com/msgpack/msgpack-javascript/compare/v2.7.1...v2.7.2 101 | 102 | * Fix a build problem in Nuxt3 projects [#200](https://github.com/msgpack/msgpack-javascript/pull/200) reported by (reported as #199 in @masaha03) 103 | 104 | ## 2.7.1 2021/09/01 105 | 106 | https://github.com/msgpack/msgpack-javascript/compare/v2.7.0...v2.7.1 107 | 108 | * No code changes 109 | * Build with TypeScript 4.4 110 | 111 | ## 2.7.0 2021/05/20 112 | 113 | https://github.com/msgpack/msgpack-javascript/compare/v2.6.3...v2.7.0 114 | 115 | * Made sure timestamp decoder to raise DecodeError in errors 116 | * This was found by fuzzing tests using [jsfuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz) 117 | * Tiny optimizations and refactoring 118 | 119 | ## 2.6.3 2021/05/04 120 | 121 | https://github.com/msgpack/msgpack-javascript/compare/v2.6.2...v2.6.3 122 | 123 | * Added `mod.ts` for Deno support 124 | 125 | ## 2.6.2 2021/05/04 126 | 127 | https://github.com/msgpack/msgpack-javascript/compare/v2.6.1...v2.6.2 128 | 129 | * Improve Deno support (see example/deno-*.ts for details) 130 | 131 | ## 2.6.1 2021/05/04 132 | 133 | https://github.com/msgpack/msgpack-javascript/compare/v2.6.0...v2.6.1 134 | 135 | * Recover Decoder instance states after `DecodeError` (mitigating [#160](https://github.com/msgpack/msgpack-javascript/issues/160)) 136 | 137 | ## 2.6.0 2021/04/21 138 | 139 | https://github.com/msgpack/msgpack-javascript/compare/v2.5.1...v2.6.0 140 | 141 | * Revert use of `tslib` (added in 2.5.0) to fix [#169](https://github.com/msgpack/msgpack-javascript/issues/169) 142 | 143 | ## v2.5.1 2021/03/21 144 | 145 | https://github.com/msgpack/msgpack-javascript/compare/v2.5.0...v2.5.1 146 | 147 | * Fixed the ESM package's dependencies 148 | ## v2.5.0 2021/03/21 149 | 150 | https://github.com/msgpack/msgpack-javascript/compare/v2.4.1...v2.5.0 151 | 152 | * Throws `DecodeError` in decoding errors 153 | * Rejects `__proto__` as a map key, throwing `DecodeError` 154 | * Thank you to Ninevra Leanne Walden for reporting this issue 155 | * Added `tslib` as a dependency 156 | 157 | ## v2.4.1 2021/03/01 158 | 159 | https://github.com/msgpack/msgpack-javascript/compare/v2.4.0...v2.4.1 160 | 161 | * Fixed a performance regression that `TextEncoder` and `TextDecoder` were never used even if available ([reported as #157 by @ChALkeR](https://github.com/msgpack/msgpack-javascript/issues/157)) 162 | 163 | ## v2.4.0 2021/02/15 164 | 165 | https://github.com/msgpack/msgpack-javascript/compare/v2.3.1...v2.4.0 166 | 167 | * Renamed `decodeStream()` to `decodeMultiStream()` 168 | * `decodeStream()` is kept as a deprecated function but will be removed in a future 169 | * Added `decodeMulti()`, a synchronous variant for `decodeMultiStream()` (thanks to @Bilge for the request in [#152](https://github.com/msgpack/msgpack-javascript/issues/152)) 170 | * Improved `decodeAsync()` and its family to accept `BufferSource` (thanks to @rajaybasu for the suggestion in [#152-issuecomment-778712021)](https://github.com/msgpack/msgpack-javascript/issues/152#issuecomment-778712021)) 171 | 172 | ## v2.3.1 2021/02/13 173 | 174 | https://github.com/msgpack/msgpack-javascript/compare/v2.3.0...v2.3.1 175 | 176 | * Fixed a lot of typos 177 | * Update dev environment: 178 | * Migration to GitHub Actions 179 | * Upgrade Webpack from v4 to v5 180 | * Enable `noImplicitReturns` and `noUncheckedIndexedAccess` in tsconfig 181 | 182 | ## v2.3.0 2020/10/17 183 | 184 | https://github.com/msgpack/msgpack-javascript/compare/v2.2.1...v2.3.0 185 | 186 | * Change the extension of ESM files from `.js` to `.mjs` [#144](https://github.com/msgpack/msgpack-javascript/pull/144) 187 | * Make the package work with `strictNullChecks: false` [#139](https://github.com/msgpack/msgpack-javascript/pull/139) by @bananaumai 188 | 189 | ## v2.2.1 2020/10/11 190 | 191 | https://github.com/msgpack/msgpack-javascript/compare/v2.2.0...v2.2.1 192 | 193 | * Fix `package.json` for webpack to use `module` field 194 | 195 | ## v2.2.0 2020/10/04 196 | 197 | https://github.com/msgpack/msgpack-javascript/compare/v2.1.1...v2.2.0 198 | 199 | * Now `package.json` has a `module` field to support ES modules 200 | 201 | ## v2.1.1 2020/10/04 202 | 203 | https://github.com/msgpack/msgpack-javascript/compare/v2.1.0...v2.1.1 204 | 205 | * Fixed typos 206 | * Refactored the codebase 207 | 208 | ## v2.1.0 2020/09/21 209 | 210 | https://github.com/msgpack/msgpack-javascript/compare/v2.0.0...v2.1.0 211 | 212 | * Added `forceIntegerToFloat` option to `EncodeOptions` by @carbotaniuman ([#123](https://github.com/msgpack/msgpack-javascript/pull/123)) 213 | 214 | ## v2.0.0 2020/09/06 215 | 216 | https://github.com/msgpack/msgpack-javascript/compare/v1.12.2...v2.0.0 217 | 218 | * Officially introduce direct use of `Encoder` and `Decoder` for better performance 219 | * The major version was bumped because it changed the interface to `Encoder` and `Decoder` 220 | * Build with TypeScript 4.0 221 | 222 | ## v1.12.2 2020/05/14 223 | 224 | https://github.com/msgpack/msgpack-javascript/compare/v1.12.1...v1.12.2 225 | 226 | * Build with TypeScript 3.9 227 | 228 | ## v1.12.1 2020/04/08 229 | 230 | https://github.com/msgpack/msgpack-javascript/compare/v1.12.0...v1.12.1 231 | 232 | * Build with TypeScript 3.8 233 | 234 | ## v1.12.0 2020/03/03 235 | 236 | https://github.com/msgpack/msgpack-javascript/compare/v1.11.1...v1.12.0 237 | 238 | * Add `EncodeOptions#ignoreUndefined` [#107](https://github.com/msgpack/msgpack-javascript/pull/107) 239 | * Like `JSON.stringify()`, less payload size, but taking more time to encode 240 | 241 | ## v1.11.1 2020/02/26 242 | 243 | https://github.com/msgpack/msgpack-javascript/compare/v1.11.0...v1.11.1 244 | 245 | * Fix use of `process.env` for browsers (#104) 246 | 247 | ## v1.11.0 2020/01/15 248 | 249 | https://github.com/msgpack/msgpack-javascript/compare/v1.10.1...v1.11.0 250 | 251 | * Added support for custom context for keeping track of objects ([#101](https://github.com/msgpack/msgpack-javascript/pull/101) by @grantila) 252 | * Export ``EncodeOptions` and `DecodeOptions` ([#100](https://github.com/msgpack/msgpack-javascript/pull/100)) 253 | 254 | ## v1.10.1 2020/01/11 255 | 256 | https://github.com/msgpack/msgpack-javascript/compare/v1.10.0...v1.10.1 257 | 258 | * Re-package it with the latest Webpack and Terser 259 | 260 | ## v1.10.0 2019/12/27 261 | 262 | https://github.com/msgpack/msgpack-javascript/compare/v1.9.3...v1.10.0 263 | 264 | * Remove WebAssembly implementation, which introduced complexity rather than performance ([#95](https://github.com/msgpack/msgpack-javascript/pull/95)) 265 | 266 | ## v1.9.3 2019/10/30 267 | 268 | https://github.com/msgpack/msgpack-javascript/compare/v1.9.2...v1.9.3 269 | 270 | * Fix a possible crash in decoding long strings (amending #88): [#90](https://github.com/msgpack/msgpack-javascript/pull/90) by @chrisnojima 271 | 272 | 273 | ## v1.9.2 2019/10/30 274 | 275 | https://github.com/msgpack/msgpack-javascript/compare/v1.9.1...v1.9.2 276 | 277 | * Fix a possible crash in decoding long strings: [#88](https://github.com/msgpack/msgpack-javascript/pull/88) by @chrisnojima 278 | 279 | ## v1.9.1 2019/09/20 280 | 281 | https://github.com/msgpack/msgpack-javascript/compare/v1.9.0...v1.9.1 282 | 283 | * No code changes from 1.9.0 284 | * Upgrade dev dependencies 285 | 286 | ## v1.9.0 2019/08/31 287 | 288 | https://github.com/msgpack/msgpack-javascript/compare/v1.8.0...v1.9.0 289 | 290 | * [Make cachedKeyDecoder configurable by sergeyzenchenko · Pull Request \#85](https://github.com/msgpack/msgpack-javascript/pull/85) 291 | * [Add support for numbers as map keys by sergeyzenchenko · Pull Request \#84](https://github.com/msgpack/msgpack-javascript/pull/84) 292 | * Build with TypeScript 3.6 293 | 294 | ## v1.8.0 2019/08/07 295 | 296 | https://github.com/msgpack/msgpack-javascript/compare/v1.7.0...v1.8.0 297 | 298 | * Adjust internal cache size according to benchmark results [bc5e681](https://github.com/msgpack/msgpack-javascript/commit/bc5e681e781881ed27efaf97ba4156b484dc7648) 299 | * Internal refactoring [#82](https://github.com/msgpack/msgpack-javascript7/pull/82) 300 | 301 | ## v1.7.0 2019/08/2 302 | 303 | https://github.com/msgpack/msgpack-javascript/compare/v1.6.0...v1.7.0 304 | 305 | * Introduce cache for map keys, which improves decoding in 1.5x faster for the benchmark (@sergeyzenchenko) [#54](https://github.com/msgpack/msgpack-javascript/pull/54) 306 | * 307 | 308 | ## v1.6.0 2019/07/19 309 | 310 | https://github.com/msgpack/msgpack-javascript/compare/v1.5.0...v1.6.0 311 | 312 | * Add `EncodeOptions.forceFloat32` to encode non-integer numbers in float32 (default to float64) [#79](https://github.com/msgpack/msgpack-javascript/pull/79) 313 | 314 | ## v1.5.0 2019/07/17 315 | 316 | https://github.com/msgpack/msgpack-javascript/compare/v1.4.6...v1.5.0 317 | 318 | * Improve `decode()` to handle `ArrayBuffer` [#78](https://github.com/msgpack/msgpack-javascript/pull/78) 319 | 320 | ## v1.4.6 2019/07/09 321 | 322 | https://github.com/msgpack/msgpack-javascript/compare/v1.4.5...v1.4.6 323 | 324 | * use `TextEncoder` to encode string in UTF-8 for performance [#68](https://github.com/msgpack/msgpack-javascript/pull/68) 325 | 326 | ## v1.4.5 2019/06/24 327 | 328 | https://github.com/msgpack/msgpack-javascript/compare/v1.4.4...v1.4.5 329 | 330 | * Fix an encoding result of -128 from int16 to int8 [#73](https://github.com/msgpack/msgpack-javascript/pull/73) 331 | 332 | ## v1.4.4 2019/06/22 333 | 334 | https://github.com/msgpack/msgpack-javascript/compare/v1.4.1...v1.4.4 335 | 336 | * Fix the UMD build setting to correctly setup `MessagePack` module in the global object 337 | 338 | ## v1.4.3, v1.4.2 339 | 340 | Mispackaged. 341 | 342 | ## v1.4.1 2019/06/22 343 | 344 | https://github.com/msgpack/msgpack-javascript/compare/v1.4.0...v1.4.1 345 | 346 | * Improved entrypoints for browsers: 347 | * Build as UMD 348 | * Minidifed by default 349 | 350 | ## v1.4.0 2019/06/12 351 | 352 | https://github.com/msgpack/msgpack-javascript/compare/v1.3.2...v1.4.0 353 | 354 | * Added `sortKeys: boolean` option to `encode()` for canonical encoding [#64](https://github.com/msgpack/msgpack-javascript/pull/64) 355 | * Fixed `RangeError` in encoding BLOB [#66](https://github.com/msgpack/msgpack-javascript/pull/66) 356 | 357 | ## v1.3.2 2019/06/04 358 | 359 | https://github.com/msgpack/msgpack-javascript/compare/v1.3.1...v1.3.2 360 | 361 | * Fix typings for older TypeScript [#55](https://github.com/msgpack/msgpack-javascript/pull/55) 362 | 363 | ## v1.3.1 2019/06/01 364 | 365 | https://github.com/msgpack/msgpack-javascript/compare/v1.3.0...v1.3.1 366 | 367 | * Fix missing exports of `decodeStream()` 368 | 369 | ## v1.3.0 2019/05/29 370 | 371 | https://github.com/msgpack/msgpack-javascript/compare/v1.2.3...v1.3.0 372 | 373 | * Add `decodeArrayStream()` to decode an array and returns `AsyncIterable` [#42](https://github.com/msgpack/msgpack-javascript/pull/42) 374 | * Add `decodeStream()` to decode an unlimited data stream [#46](https://github.com/msgpack/msgpack-javascript/pull/46) 375 | * Let `decodeAsync()` and `decodeArrayStream()` to take `ReadalbeStream>` (whatwg-streams) [#43](https://github.com/msgpack/msgpack-javascript/pull/46) 376 | 377 | ## v1.2.3 2019/05/29 378 | 379 | https://github.com/msgpack/msgpack-javascript/compare/v1.2.2...v1.2.3 380 | 381 | * More optimizations for string decoding performance 382 | 383 | ## v1.2.2 2019/05/29 384 | 385 | https://github.com/msgpack/msgpack-javascript/compare/v1.2.1...v1.2.2 386 | 387 | * Improved array decoding performance ([#32](https://github.com/msgpack/msgpack-javascript/pull/32) by @sergeyzenchenko) 388 | * Improved string decoding performance with TextDecoder ([#34](https://github.com/msgpack/msgpack-javascript/pull/34) by @sergeyzenchenko) 389 | 390 | ## v1.2.1 2019/05/26 391 | 392 | https://github.com/msgpack/msgpack-javascript/compare/v1.2.0...v1.2.1 393 | 394 | * Reduced object allocations in `encode()` 395 | 396 | ## v1.2.0 2019/05/25 397 | 398 | https://github.com/msgpack/msgpack-javascript/compare/v1.1.0...v1.2.0 399 | 400 | * Shipped with WebAssembly ([#26](https://github.com/msgpack/msgpack-javascript/pull/26)) 401 | * Fix handling strings to keep lone surrogates 402 | * Fix issues in decoding very large string, which caused RangeError 403 | 404 | ## v1.1.0 2019/05/19 405 | 406 | https://github.com/msgpack/msgpack-javascript/compare/v1.0.0...v1.1.0 407 | 408 | * Add options to `decode()` and `decodeAsync()`: 409 | `maxStrLength`, `maxBinLength`, `maxArrayLength`, `maxMapLength`, and `maxExtLength` to limit max length of each item 410 | 411 | ## v1.0.1 2019/05/12 412 | 413 | https://github.com/msgpack/msgpack-javascript/compare/v1.0.0...v1.0.1 414 | 415 | * Fix IE11 incompatibility 416 | 417 | ## v1.0.0 2019/05/11 418 | 419 | * Initial stable release 420 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 The MessagePack Community. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | npm run test 4 | 5 | test-all: 6 | npm ci 7 | npm publish --dry-run --tag "$(shell node --experimental-strip-types tools/get-release-tag.mjs)" 8 | 9 | publish: validate-git-status 10 | npm publish --tag "$(shell node --experimental-strip-types tools/get-release-tag.mjs)" 11 | git push origin main 12 | git push origin --tags 13 | 14 | validate-git-status: 15 | @ if [ "`git symbolic-ref --short HEAD`" != "main" ] ; \ 16 | then echo "Not on the main branch!\n" ; exit 1 ; \ 17 | fi 18 | @ if ! git diff --exit-code --quiet ; \ 19 | then echo "Local differences!\n" ; git status ; exit 1 ; \ 20 | fi 21 | git pull 22 | 23 | profile-encode: 24 | npx rimraf isolate-*.log 25 | node --prof --require ts-node/register -e 'require("./benchmark/profile-encode")' 26 | node --prof-process --preprocess -j isolate-*.log | npx flamebearer 27 | 28 | profile-decode: 29 | npx rimraf isolate-*.log 30 | node --prof --require ts-node/register -e 'require("./benchmark/profile-decode")' 31 | node --prof-process --preprocess -j isolate-*.log | npx flamebearer 32 | 33 | benchmark: 34 | npx node -r ts-node/register benchmark/benchmark-from-msgpack-lite.ts 35 | @echo 36 | node benchmark/msgpack-benchmark.js 37 | 38 | .PHONY: test dist validate-branch benchmark 39 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /benchmark/benchmark-from-msgpack-lite-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "int0": 0, 3 | "int1": 1, 4 | "int1-": -1, 5 | "int8": 255, 6 | "int8-": -255, 7 | "int16": 256, 8 | "int16-": -256, 9 | "int32": 65536, 10 | "int32-": -65536, 11 | "nil": null, 12 | "true": true, 13 | "false": false, 14 | "float": 0.5, 15 | "float-": -0.5, 16 | "string0": "", 17 | "string1": "A", 18 | "string4": "foobarbaz", 19 | "string8": "Omnes viae Romam ducunt.", 20 | "string16": "L’homme n’est qu’un roseau, le plus faible de la nature ; mais c’est un roseau pensant. Il ne faut pas que l’univers entier s’arme pour l’écraser : une vapeur, une goutte d’eau, suffit pour le tuer. Mais, quand l’univers l’écraserait, l’homme serait encore plus noble que ce qui le tue, puisqu’il sait qu’il meurt, et l’avantage que l’univers a sur lui, l’univers n’en sait rien. Toute notre dignité consiste donc en la pensée. C’est de là qu’il faut nous relever et non de l’espace et de la durée, que nous ne saurions remplir. Travaillons donc à bien penser : voilà le principe de la morale.", 21 | "array0": [], 22 | "array1": [ 23 | "foo" 24 | ], 25 | "array8": [ 26 | 1, 27 | 2, 28 | 4, 29 | 8, 30 | 16, 31 | 32, 32 | 64, 33 | 128, 34 | 256, 35 | 512, 36 | 1024, 37 | 2048, 38 | 4096, 39 | 8192, 40 | 16384, 41 | 32768, 42 | 65536, 43 | 131072, 44 | 262144, 45 | 524288, 46 | 1048576 47 | ], 48 | "map0": {}, 49 | "map1": { 50 | "foo": "bar" 51 | } 52 | } -------------------------------------------------------------------------------- /benchmark/benchmark-from-msgpack-lite.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // original: https://raw.githubusercontent.com/kawanet/msgpack-lite/master/lib/benchmark.js 3 | 4 | var msgpack_msgpack = require("../src"); 5 | 6 | var msgpack_node = try_require("msgpack"); 7 | var msgpack_lite = try_require("msgpack-lite"); 8 | var msgpack_js = try_require("msgpack-js"); 9 | var msgpackr = try_require("msgpackr"); 10 | var msgpack5 = try_require("msgpack5"); 11 | var notepack = try_require("notepack"); 12 | 13 | msgpack5 = msgpack5 && msgpack5(); 14 | 15 | var pkg = require("../package.json"); 16 | var data = require("./benchmark-from-msgpack-lite-data.json"); 17 | var packed = msgpack_lite.encode(data); 18 | var expected = JSON.stringify(data); 19 | 20 | var argv = Array.prototype.slice.call(process.argv, 2); 21 | 22 | if (argv[0] === "-v") { 23 | console.warn(pkg.name + " " + pkg.version); 24 | process.exit(0); 25 | } 26 | 27 | var limit = 5; 28 | if (argv[0] - 0) limit = argv.shift() - 0; 29 | limit *= 1000; 30 | 31 | var COL1 = 65; 32 | var COL2 = 7; 33 | var COL3 = 5; 34 | var COL4 = 7; 35 | 36 | const v8version = process.versions.v8.split(/\./, 2).join('.'); 37 | console.log(`Benchmark on NodeJS/${process.version} (V8/${v8version})\n`) 38 | console.log(rpad("operation", COL1), "|", " op ", "|", " ms ", "|", " op/s "); 39 | console.log(rpad("", COL1, "-"), "|", lpad(":", COL2, "-"), "|", lpad(":", COL3, "-"), "|", lpad(":", COL4, "-")); 40 | 41 | var buf, obj; 42 | 43 | if (JSON) { 44 | buf = bench('buf = Buffer.from(JSON.stringify(obj));', JSON_stringify, data); 45 | obj = bench('obj = JSON.parse(buf.toString("utf-8"));', JSON_parse, buf); 46 | runTest(obj); 47 | } 48 | 49 | if (msgpack_lite) { 50 | buf = bench('buf = require("msgpack-lite").encode(obj);', msgpack_lite.encode, data); 51 | obj = bench('obj = require("msgpack-lite").decode(buf);', msgpack_lite.decode, packed); 52 | runTest(obj); 53 | } 54 | 55 | if (msgpack_node) { 56 | buf = bench('buf = require("msgpack").pack(obj);', msgpack_node.pack, data); 57 | obj = bench('obj = require("msgpack").unpack(buf);', msgpack_node.unpack, buf); 58 | runTest(obj); 59 | } 60 | 61 | if (msgpack_msgpack) { 62 | buf = bench('buf = require("@msgpack/msgpack").encode(obj);', msgpack_msgpack.encode, data); 63 | obj = bench('obj = require("@msgpack/msgpack").decode(buf);', msgpack_msgpack.decode, buf); 64 | runTest(obj); 65 | 66 | const encoder = new msgpack_msgpack.Encoder(); 67 | const decoder = new msgpack_msgpack.Decoder(); 68 | buf = bench('buf = /* @msgpack/msgpack */ encoder.encode(obj);', (data) => encoder.encode(data), data); 69 | obj = bench('obj = /* @msgpack/msgpack */ decoder.decode(buf);', (buf) => decoder.decode(buf), buf); 70 | runTest(obj); 71 | 72 | if (process.env["CACHE_HIT_RATE"]) { 73 | const {hit, miss} = decoder.keyDecoder; 74 | console.log(`CACHE_HIT_RATE: cache hit rate in CachedKeyDecoder: hit=${hit}, miss=${miss}, hit rate=${hit / (hit + miss)}`); 75 | } 76 | } 77 | 78 | if (msgpackr) { 79 | buf = bench('buf = require("msgpackr").pack(obj);', msgpackr.pack, data); 80 | obj = bench('obj = require("msgpackr").unpack(buf);', msgpackr.unpack, buf); 81 | runTest(obj); 82 | } 83 | 84 | if (msgpack_js) { 85 | buf = bench('buf = require("msgpack-js").encode(obj);', msgpack_js.encode, data); 86 | obj = bench('obj = require("msgpack-js").decode(buf);', msgpack_js.decode, buf); 87 | runTest(obj); 88 | } 89 | 90 | if (msgpack5) { 91 | buf = bench('buf = require("msgpack5")().encode(obj);', msgpack5.encode, data); 92 | obj = bench('obj = require("msgpack5")().decode(buf);', msgpack5.decode, buf); 93 | runTest(obj); 94 | } 95 | 96 | if (notepack) { 97 | buf = bench('buf = require("notepack").encode(obj);', notepack.encode, data); 98 | obj = bench('obj = require("notepack").decode(buf);', notepack.decode, buf); 99 | runTest(obj); 100 | } 101 | 102 | function JSON_stringify(src: any): Buffer { 103 | return Buffer.from(JSON.stringify(src)); 104 | } 105 | 106 | function JSON_parse(json: Buffer): any { 107 | return JSON.parse(json.toString("utf-8")); 108 | } 109 | 110 | function bench(name: string, func: (...args: any[]) => any, src: any) { 111 | if (argv.length) { 112 | var match = argv.filter(function(grep) { 113 | return (name.indexOf(grep) > -1); 114 | }); 115 | if (!match.length) return SKIP; 116 | } 117 | // warm up 118 | func(src); 119 | 120 | var ret, duration = 0; 121 | var start = Date.now(); 122 | var count = 0; 123 | while (1) { 124 | var end = Date.now(); 125 | duration = end - start; 126 | if (duration >= limit) break; 127 | while ((++count) % 100) ret = func(src); 128 | } 129 | name = rpad(name, COL1); 130 | var score = Math.floor(count / duration! * 1000); 131 | console.log(name, "|", lpad(`${count}`, COL2), "|", lpad(`${duration}`, COL3), "|", lpad(`${score}`, COL4)); 132 | return ret; 133 | } 134 | 135 | function rpad(str: string, len: number, chr = " ") { 136 | return str.padEnd(len, chr); 137 | } 138 | 139 | function lpad(str: string, len: number, chr = " ") { 140 | return str.padStart(len, chr); 141 | } 142 | 143 | function runTest(actual: any) { 144 | if (actual === SKIP) return; 145 | actual = JSON.stringify(actual); 146 | if (actual === expected) return; 147 | console.warn("expected: " + expected); 148 | console.warn("actual: " + actual); 149 | } 150 | 151 | function SKIP() { 152 | } 153 | 154 | function try_require(name: string) { 155 | try { 156 | return require(name); 157 | } catch (e) { 158 | // ignore 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /benchmark/decode-string.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { utf8EncodeJs, utf8Count, utf8DecodeJs, utf8DecodeTD } from "../src/utils/utf8"; 3 | 4 | // @ts-ignore 5 | import Benchmark from "benchmark"; 6 | 7 | for (const baseStr of ["A", "あ", "🌏"]) { 8 | const dataSet = [10, 100, 500, 1_000].map((n) => { 9 | return baseStr.repeat(n); 10 | }); 11 | 12 | for (const str of dataSet) { 13 | const byteLength = utf8Count(str); 14 | const bytes = new Uint8Array(new ArrayBuffer(byteLength)); 15 | utf8EncodeJs(str, bytes, 0); 16 | 17 | console.log(`\n## string "${baseStr}" (strLength=${str.length}, byteLength=${byteLength})\n`); 18 | 19 | const suite = new Benchmark.Suite(); 20 | 21 | suite.add("utf8DecodeJs", () => { 22 | if (utf8DecodeJs(bytes, 0, byteLength) !== str) { 23 | throw new Error("wrong result!"); 24 | } 25 | }); 26 | 27 | suite.add("TextDecoder", () => { 28 | if (utf8DecodeTD(bytes, 0, byteLength) !== str) { 29 | throw new Error("wrong result!"); 30 | } 31 | }); 32 | suite.on("cycle", (event: any) => { 33 | console.log(String(event.target)); 34 | }); 35 | 36 | suite.run(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /benchmark/encode-string.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { utf8EncodeJs, utf8Count, utf8EncodeTE } from "../src/utils/utf8"; 3 | 4 | // @ts-ignore 5 | import Benchmark from "benchmark"; 6 | 7 | for (const baseStr of ["A", "あ", "🌏"]) { 8 | const dataSet = [10, 30, 50, 100].map((n) => { 9 | return baseStr.repeat(n); 10 | }); 11 | 12 | for (const str of dataSet) { 13 | const byteLength = utf8Count(str); 14 | const buffer = new Uint8Array(byteLength); 15 | 16 | console.log(`\n## string "${baseStr}" (strLength=${str.length}, byteLength=${byteLength})\n`); 17 | 18 | const suite = new Benchmark.Suite(); 19 | 20 | suite.add("utf8EncodeJs", () => { 21 | utf8EncodeJs(str, buffer, 0); 22 | }); 23 | 24 | suite.add("utf8DecodeTE", () => { 25 | utf8EncodeTE(str, buffer, 0); 26 | }); 27 | suite.on("cycle", (event: any) => { 28 | console.log(String(event.target)); 29 | }); 30 | 31 | suite.run(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /benchmark/key-decoder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { utf8EncodeJs, utf8Count, utf8DecodeJs } from "../src/utils/utf8"; 3 | 4 | // @ts-ignore 5 | import Benchmark from "benchmark"; 6 | import { CachedKeyDecoder } from "../src/CachedKeyDecoder"; 7 | 8 | type InputType = { 9 | bytes: Uint8Array; 10 | byteLength: number; 11 | str: string; 12 | }; 13 | 14 | const keys: Array = Object.keys(require("./benchmark-from-msgpack-lite-data.json")).map((str) => { 15 | const byteLength = utf8Count(str); 16 | const bytes = new Uint8Array(new ArrayBuffer(byteLength)); 17 | utf8EncodeJs(str, bytes, 0); 18 | return { bytes, byteLength, str }; 19 | }); 20 | 21 | for (const dataSet of [keys]) { 22 | const keyDecoder = new CachedKeyDecoder(); 23 | // make cache storage full 24 | for (let i = 0; i < keyDecoder.maxKeyLength; i++) { 25 | for (let j = 0; j < keyDecoder.maxLengthPerKey; j++) { 26 | const str = `${j.toString().padStart(i + 1, "0")}`; 27 | const byteLength = utf8Count(str); 28 | const bytes = new Uint8Array(new ArrayBuffer(byteLength)); 29 | utf8EncodeJs(str, bytes, 0); 30 | keyDecoder.decode(bytes, 0, byteLength); // fill 31 | } 32 | } 33 | 34 | // console.dir(keyDecoder, { depth: 100 }); 35 | console.log("## When the cache storage is full."); 36 | 37 | const suite = new Benchmark.Suite(); 38 | 39 | suite.add("utf8DecodeJs", () => { 40 | for (const data of dataSet) { 41 | if (utf8DecodeJs(data.bytes, 0, data.byteLength) !== data.str) { 42 | throw new Error("wrong result!"); 43 | } 44 | } 45 | }); 46 | 47 | suite.add("CachedKeyDecoder", () => { 48 | for (const data of dataSet) { 49 | if (keyDecoder.decode(data.bytes, 0, data.byteLength) !== data.str) { 50 | throw new Error("wrong result!"); 51 | } 52 | } 53 | }); 54 | suite.on("cycle", (event: any) => { 55 | console.log(String(event.target)); 56 | }); 57 | 58 | suite.run(); 59 | } 60 | -------------------------------------------------------------------------------- /benchmark/msgpack-benchmark.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // based on https://github.com/endel/msgpack-benchmark 3 | "use strict"; 4 | require("ts-node/register"); 5 | const Benchmark = require("benchmark"); 6 | 7 | const msgpackEncode = require("..").encode; 8 | const msgpackDecode = require("..").decode; 9 | const ExtensionCodec = require("..").ExtensionCodec; 10 | 11 | const float32ArrayExtensionCodec = new ExtensionCodec(); 12 | float32ArrayExtensionCodec.register({ 13 | type: 0x01, 14 | encode: (object) => { 15 | if (object instanceof Float32Array) { 16 | return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); 17 | } 18 | return null; 19 | }, 20 | decode: (data) => { 21 | const copy = new Uint8Array(data.byteLength); 22 | copy.set(data); 23 | return new Float32Array(copy.buffer); 24 | }, 25 | }); 26 | 27 | const float32ArrayZeroCopyExtensionCodec = new ExtensionCodec(); 28 | float32ArrayZeroCopyExtensionCodec.register({ 29 | type: 0x01, 30 | encode: (object) => { 31 | if (object instanceof Float32Array) { 32 | return (pos) => { 33 | const bpe = Float32Array.BYTES_PER_ELEMENT; 34 | const padding = 1 + ((bpe - ((pos + 1) % bpe)) % bpe); 35 | const data = new Uint8Array(object.buffer); 36 | const result = new Uint8Array(padding + data.length); 37 | result[0] = padding; 38 | result.set(data, padding); 39 | return result; 40 | }; 41 | } 42 | return null; 43 | }, 44 | decode: (data) => { 45 | const padding = data[0]; 46 | const bpe = Float32Array.BYTES_PER_ELEMENT; 47 | const offset = data.byteOffset + padding; 48 | const length = data.byteLength - padding; 49 | return new Float32Array(data.buffer, offset, length / bpe); 50 | }, 51 | }); 52 | 53 | const implementations = { 54 | "@msgpack/msgpack": { 55 | encode: msgpackEncode, 56 | decode: msgpackDecode, 57 | }, 58 | "@msgpack/msgpack (Float32Array extension)": { 59 | encode: (data) => msgpackEncode(data, { extensionCodec: float32ArrayExtensionCodec }), 60 | decode: (data) => msgpackDecode(data, { extensionCodec: float32ArrayExtensionCodec }), 61 | }, 62 | "@msgpack/msgpack (Float32Array with zero-copy extension)": { 63 | encode: (data) => msgpackEncode(data, { extensionCodec: float32ArrayZeroCopyExtensionCodec }), 64 | decode: (data) => msgpackDecode(data, { extensionCodec: float32ArrayZeroCopyExtensionCodec }), 65 | }, 66 | "msgpack-lite": { 67 | encode: require("msgpack-lite").encode, 68 | decode: require("msgpack-lite").decode, 69 | }, 70 | "notepack.io": { 71 | encode: require("notepack.io/browser/encode"), 72 | decode: require("notepack.io/browser/decode"), 73 | }, 74 | }; 75 | 76 | const samples = [ 77 | { 78 | // exactly the same as: 79 | // https://raw.githubusercontent.com/endel/msgpack-benchmark/master/sample-large.json 80 | name: "./sample-large.json", 81 | data: require("./sample-large.json"), 82 | }, 83 | { 84 | name: "Large array of numbers", 85 | data: [ 86 | { 87 | position: new Array(1e3).fill(1.14), 88 | }, 89 | ], 90 | }, 91 | { 92 | name: "Large Float32Array", 93 | data: [ 94 | { 95 | position: new Float32Array(1e3).fill(1.14), 96 | }, 97 | ], 98 | }, 99 | ]; 100 | 101 | function validate(name, data, encoded) { 102 | return JSON.stringify(data) === JSON.stringify(implementations[name].decode(encoded)); 103 | } 104 | 105 | for (const sample of samples) { 106 | const { name: sampleName, data } = sample; 107 | const encodeSuite = new Benchmark.Suite(); 108 | const decodeSuite = new Benchmark.Suite(); 109 | 110 | console.log(""); 111 | console.log("**" + sampleName + ":** (" + JSON.stringify(data).length + " bytes in JSON)"); 112 | console.log(""); 113 | 114 | for (const name of Object.keys(implementations)) { 115 | implementations[name].toDecode = implementations[name].encode(data); 116 | if (!validate(name, data, implementations[name].toDecode)) { 117 | console.log("```"); 118 | console.log("Not supported by " + name); 119 | console.log("```"); 120 | continue; 121 | } 122 | encodeSuite.add("(encode) " + name, () => { 123 | implementations[name].encode(data); 124 | }); 125 | decodeSuite.add("(decode) " + name, () => { 126 | implementations[name].decode(implementations[name].toDecode); 127 | }); 128 | } 129 | encodeSuite.on("cycle", (event) => { 130 | console.log(String(event.target)); 131 | }); 132 | 133 | console.log("```"); 134 | encodeSuite.run(); 135 | console.log("```"); 136 | 137 | console.log(""); 138 | 139 | decodeSuite.on("cycle", (event) => { 140 | console.log(String(event.target)); 141 | }); 142 | 143 | console.log("```"); 144 | decodeSuite.run(); 145 | console.log("```"); 146 | } 147 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@msgpack/msgpack-benchmark", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "update-dependencies": "npx rimraf node_modules/ package-lock.json ; npm install ; npm audit fix --force ; git restore package.json ; npm install" 7 | }, 8 | "dependencies": { 9 | "benchmark": "latest", 10 | "msgpack-lite": "latest", 11 | "msgpackr": "latest", 12 | "notepack.io": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /benchmark/profile-decode.ts: -------------------------------------------------------------------------------- 1 | import { encode, decode, decodeAsync } from "../src"; 2 | // @ts-ignore 3 | import _ from "lodash"; 4 | const data = require("./benchmark-from-msgpack-lite-data.json"); 5 | const dataX = _.cloneDeep(new Array(100).fill(data)); 6 | const encoded = encode(dataX); 7 | 8 | console.log("encoded size:", encoded.byteLength); 9 | 10 | console.time("decode #1"); 11 | for (let i = 0; i < 1000; i++) { 12 | decode(encoded); 13 | } 14 | console.timeEnd("decode #1"); 15 | 16 | (async () => { 17 | const buffers = async function*() { 18 | yield encoded; 19 | }; 20 | 21 | console.time("decodeAsync #1"); 22 | for (let i = 0; i < 1000; i++) { 23 | await decodeAsync(buffers()); 24 | } 25 | console.timeEnd("decodeAsync #1"); 26 | })(); 27 | -------------------------------------------------------------------------------- /benchmark/profile-encode.ts: -------------------------------------------------------------------------------- 1 | import { encode } from "../src"; 2 | // @ts-ignore 3 | import _ from "lodash"; 4 | 5 | const data = require("./benchmark-from-msgpack-lite-data.json"); 6 | const dataX = _.cloneDeep(new Array(100).fill(data)); 7 | 8 | console.time("encode #1"); 9 | for (let i = 0; i < 1000; i++) { 10 | encode(dataX); 11 | } 12 | console.timeEnd("encode #1"); 13 | 14 | console.time("encode #2"); 15 | for (let i = 0; i < 1000; i++) { 16 | encode(dataX); 17 | } 18 | console.timeEnd("encode #2"); 19 | -------------------------------------------------------------------------------- /benchmark/sample-large.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id":"56490c18d9275a0003000000", 4 | "author":null, 5 | "created_at":"2015-11-15T22:50:00.170Z", 6 | "description":"A weekly discussion by Ruby developers about programming, life, and careers.", 7 | "image":"https://s3.amazonaws.com/devchat.tv/ruby-rogues-thumb.jpg", 8 | "keywords":[ 9 | "Business", 10 | "Careers", 11 | "Technology", 12 | "Software How-To" 13 | ], 14 | "language":"en", 15 | "permalink":"http://rubyrogues.com/", 16 | "published":true, 17 | "title":"The Ruby Rogues", 18 | "updated_at":"2015-11-15T22:50:06.565Z", 19 | "url":"http://feeds.feedwrench.com/RubyRogues.rss", 20 | "score1": 100, 21 | "score2": 0.1 22 | }, 23 | { 24 | "_id":"56490d6ad9275a00030000eb", 25 | "author":null, 26 | "created_at":"2015-11-15T22:55:38.074Z", 27 | "description":"Um podcast feito para programadores e empreendedores.", 28 | "image":"http://www.grokpodcast.com/images/logo_itunes_grande.png", 29 | "keywords":[ 30 | "Technology", 31 | "Podcasting", 32 | "Business", 33 | "Careers" 34 | ], 35 | "language":"pt-BR", 36 | "permalink":"http://www.grokpodcast.com/", 37 | "published":true, 38 | "title":"Grok Podcast", 39 | "updated_at":"2015-11-15T22:55:47.498Z", 40 | "url":"http://www.grokpodcast.com/atom.xml", 41 | "score1": 100, 42 | "score2": 0.1 43 | }, 44 | { 45 | "_id":"564a1c30b1191d0003000000", 46 | "author":null, 47 | "created_at":"2015-11-16T18:10:56.610Z", 48 | "description":"The Web Platform Podcast is a developer discussion that dives deep into ‘all things’ web. We discuss everything from developing for mobile to building HDTV software. From wearables \u0026 robotics to user experience \u0026 mentoring, we bring to our listeners everything related to building products \u0026 services for The Web Platform of today, tomorrow, and beyond.", 49 | "image":"http://static.libsyn.com/p/assets/f/7/2/0/f7208dae16d0543e/twp-logo-flat-blue-square.png", 50 | "keywords":[ 51 | "Technology", 52 | "Software How-To", 53 | "Tech News" 54 | ], 55 | "language":"en", 56 | "permalink":"http://thewebplatform.libsyn.com/webpage", 57 | "published":true, 58 | "title":"The Web Platform Podcast", 59 | "updated_at":"2015-11-16T18:11:02.022Z", 60 | "url":"http://thewebplatform.libsyn.com//rss", 61 | "score1": 100, 62 | "score2": 0.1 63 | }, 64 | { 65 | "_id":"564a1de3b1191d0003000047", 66 | "author":null, 67 | "created_at":"2015-11-16T18:18:11.854Z", 68 | "description":"Developer Tea is a podcast for web and software developers hosted by a developer that you can listen to in less than 10 minutes. The show will cover a wide variety of topics related to the career of being a developer. We hope you'll take the topics from this podcast and continue the conversation, either online or in person with your peers. The show is hosted by Jonathan Cutrell, Director of Technology at Whiteboard and the author of Hacking the Impossible, a developer's guide to working with visionaries. :: Twitter: @developertea @jcutrell :: Email: developertea@gmail.com", 69 | "image":"http://simplecast-media.s3.amazonaws.com/podcast/image/363/1440374119-artwork.jpg", 70 | "keywords":[ 71 | "Technology", 72 | "Business", 73 | "Careers", 74 | "Society \u0026 Culture" 75 | ], 76 | "language":"en-us", 77 | "permalink":"http://www.developertea.com/", 78 | "published":true, 79 | "title":"Developer Tea", 80 | "updated_at":"2015-11-16T23:00:23.224Z", 81 | "url":"http://feeds.feedburner.com/developertea", 82 | "score1": 100, 83 | "score2": 0.1 84 | }, 85 | { 86 | "_id":"564a3163e51cc0000300004c", 87 | "author":null, 88 | "created_at":"2015-11-16T19:41:23.436Z", 89 | "description":"Conference talks from the Remote Conferences series put on by Devchat.tv", 90 | "image":"https://s3.amazonaws.com/devchat.tv/RemoteConfs.jpg", 91 | "keywords":[ 92 | 93 | ], 94 | "language":"en", 95 | "permalink":"http://remoteconfs.com/", 96 | "published":true, 97 | "title":"Remote Conferences - Audio", 98 | "updated_at":"2015-11-16T19:41:24.367Z", 99 | "url":"http://feeds.feedwrench.com/remoteconfs-audio.rss", 100 | "score1": 100, 101 | "score2": 0.1 102 | }, 103 | { 104 | "_id":"564a315de51cc00003000000", 105 | "author":null, 106 | "created_at":"2015-11-16T19:41:17.492Z", 107 | "description":"Weekly discussion by freelancers and professionals about running a business, finding clients, marketing, and lifestyle related to being a freelancer.", 108 | "image":"https://s3.amazonaws.com/devchat.tv/freelancers_show_thumb.jpg", 109 | "keywords":[ 110 | "Business", 111 | "Careers", 112 | "Management \u0026amp; Marketing", 113 | "Education", 114 | "Training" 115 | ], 116 | "language":"en", 117 | "permalink":"http://www.freelancersshow.com/", 118 | "published":true, 119 | "title":"The Freelancers' Show", 120 | "updated_at":"2015-11-16T19:41:27.459Z", 121 | "url":"http://feeds.feedwrench.com/TheFreelancersShow.rss", 122 | "score1": 100, 123 | "score2": 0.1 124 | }, 125 | { 126 | "_id":"564a3169e51cc000030000cd", 127 | "author":null, 128 | "created_at":"2015-11-16T19:41:29.686Z", 129 | "description":"React Native Radio Podcast", 130 | "image":"https://s3.amazonaws.com/devchat.tv/react-native-radio-album-art.jpg", 131 | "keywords":[ 132 | 133 | ], 134 | "language":"en", 135 | "permalink":"http://devchat.tv/react-native-radio", 136 | "published":true, 137 | "title":"React Native Radio", 138 | "updated_at":"2015-11-16T19:41:29.999Z", 139 | "url":"http://feeds.feedwrench.com/react-native-radio.rss", 140 | "score1": 100, 141 | "score2": 0.1 142 | }, 143 | { 144 | "_id":"564a316fe51cc000030000d4", 145 | "author":null, 146 | "created_at":"2015-11-16T19:41:35.937Z", 147 | "description":"The iOS Development Podcast", 148 | "image":"https://s3.amazonaws.com/devchat.tv/iPhreaks-thumb.jpg", 149 | "keywords":[ 150 | "Technology", 151 | "Tech News", 152 | "Software How-To" 153 | ], 154 | "language":"en", 155 | "permalink":"http://iphreaksshow.com/", 156 | "published":true, 157 | "title":"The iPhreaks Show", 158 | "updated_at":"2015-11-16T19:41:43.700Z", 159 | "url":"http://feeds.feedwrench.com/iPhreaks.rss", 160 | "score1": 100, 161 | "score2": 0.1 162 | }, 163 | { 164 | "_id":"564a3184e51cc00003000156", 165 | "author":null, 166 | "created_at":"2015-11-16T19:41:56.874Z", 167 | "description":"Weekly podcast discussion about Javascript on the front and back ends. Also discuss programming practices, coding environments, and the communities related to the technology.", 168 | "image":"https://s3.amazonaws.com/devchat.tv/javascript_jabber_thumb.jpg", 169 | "keywords":[ 170 | "Education", 171 | "Training", 172 | "Technology", 173 | "Software How-To" 174 | ], 175 | "language":"en", 176 | "permalink":"http://javascriptjabber.com/", 177 | "published":true, 178 | "title":"JavaScript Jabber", 179 | "updated_at":"2015-11-16T19:42:24.692Z", 180 | "url":"http://feeds.feedwrench.com/JavaScriptJabber.rss", 181 | "score1": 100, 182 | "score2": 0.1 183 | }, 184 | { 185 | "_id":"564a31dee51cc00003000210", 186 | "author":null, 187 | "created_at":"2015-11-16T19:43:26.390Z", 188 | "description":"Each week we explore an aspect of web security.", 189 | "image":"http://devchat.cachefly.net/websecwarriors/logo_3000x3000.jpeg", 190 | "keywords":[ 191 | 192 | ], 193 | "language":"en", 194 | "permalink":"http://websecuritywarriors.com/", 195 | "published":true, 196 | "title":"Web Security Warriors", 197 | "updated_at":"2015-11-16T19:43:28.133Z", 198 | "url":"http://feeds.feedwrench.com/websecwarriors.rss", 199 | "score1": 100, 200 | "score2": 0.1 201 | }, 202 | { 203 | "_id":"564a3ddbe51cc00003000217", 204 | "author":null, 205 | "created_at":"2015-11-16T20:34:35.791Z", 206 | "description":"Podcasts produzidos de 2008 a 2010 sobre jogos e todos os tipos de assuntos relacionados ao universo e cultura dos vídeogames.", 207 | "image":"http://jogabilida.de/wp-content/uploads/2011/12/nl-podcast.png", 208 | "keywords":[ 209 | "Games \u0026 Hobbies", 210 | "Video Games" 211 | ], 212 | "language":"pt-BR", 213 | "permalink":"http://jogabilida.de/", 214 | "published":true, 215 | "title":"Podcast NowLoading", 216 | "updated_at":"2015-11-16T23:00:23.963Z", 217 | "url":"http://feeds.feedburner.com/podcastnowloading", 218 | "score1": 100, 219 | "score2": 0.1 220 | }, 221 | { 222 | "_id":"564b9cfe08602e00030000fa", 223 | "author":null, 224 | "created_at":"2015-11-17T21:32:46.210Z", 225 | "description":"Being Boss is a podcast for creative entrepreneurs. From Emily Thompson and Kathleen Shannon. Get your business together. Being boss is hard. Making a dream job of your own isn't easy. But getting paid for it, becoming known for it, and finding purpose in it, is so doable - if you do the work.", 226 | "image":"http://www.lovebeingboss.com/img/skin/Header_WhiteLogo.png", 227 | "keywords":[ 228 | 229 | ], 230 | "language":null, 231 | "permalink":"http://www.lovebeingboss.com/", 232 | "published":true, 233 | "title":"Being Boss // A Podcast for Creative Entrepreneurs", 234 | "updated_at":"2015-11-17T21:32:50.672Z", 235 | "url":"http://www.lovebeingboss.com/RSSRetrieve.aspx?ID=18365\u0026Type=RSS20", 236 | "score1": 100, 237 | "score2": 0.1 238 | }, 239 | { 240 | "_id":"564c5c8008602e0003000128", 241 | "author":null, 242 | "created_at":"2015-11-18T11:09:52.991Z", 243 | "description":"O mundo pop vira piada no Jovem Nerd", 244 | "image":"http://jovemnerd.ig.com.br/wp-content/themes/jovemnerd_v2b/images/NC_FEED.jpg", 245 | "keywords":[ 246 | "Society \u0026 Culture" 247 | ], 248 | "language":"pt-BR", 249 | "permalink":"http://jovemnerd.com.br/", 250 | "published":true, 251 | "title":"Nerdcast", 252 | "updated_at":"2015-11-18T11:11:20.034Z", 253 | "url":"http://jovemnerd.com.br/categoria/nerdcast/feed/", 254 | "score1": 100, 255 | "score2": 0.1 256 | } 257 | ] 258 | -------------------------------------------------------------------------------- /benchmark/string.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { encode, decode } from "../src"; 3 | 4 | const ascii = "A".repeat(40000); 5 | const emoji = "🌏".repeat(20000); 6 | 7 | { 8 | // warm up ascii 9 | const data = ascii; 10 | const encoded = encode(data); 11 | decode(encoded); 12 | console.log(`encode / decode ascii data.length=${data.length} encoded.byteLength=${encoded.byteLength}`); 13 | 14 | // run 15 | 16 | console.time("encode ascii"); 17 | for (let i = 0; i < 1000; i++) { 18 | encode(data); 19 | } 20 | console.timeEnd("encode ascii"); 21 | 22 | console.time("decode ascii"); 23 | for (let i = 0; i < 1000; i++) { 24 | decode(encoded); 25 | } 26 | console.timeEnd("decode ascii"); 27 | } 28 | 29 | { 30 | // warm up emoji 31 | const data = emoji; 32 | const encoded = encode(data); 33 | decode(encoded); 34 | 35 | console.log(`encode / decode emoji data.length=${data.length} encoded.byteLength=${encoded.byteLength}`); 36 | 37 | // run 38 | 39 | console.time("encode emoji"); 40 | for (let i = 0; i < 1000; i++) { 41 | encode(data); 42 | } 43 | console.timeEnd("encode emoji"); 44 | 45 | console.time("decode emoji"); 46 | for (let i = 0; i < 1000; i++) { 47 | decode(encoded); 48 | } 49 | console.timeEnd("decode emoji"); 50 | } 51 | -------------------------------------------------------------------------------- /benchmark/sync-vs-async.ts: -------------------------------------------------------------------------------- 1 | #!ts-node 2 | /* eslint-disable no-console */ 3 | 4 | import { encode, decode, decodeAsync, decodeArrayStream } from "../src"; 5 | import { writeFileSync, unlinkSync, readFileSync, createReadStream } from "fs"; 6 | import { deepStrictEqual } from "assert"; 7 | 8 | (async () => { 9 | const data = []; 10 | for (let i = 0; i < 1000; i++) { 11 | const id = i + 1; 12 | data.push({ 13 | id, 14 | score: Math.round(Math.random() * Number.MAX_SAFE_INTEGER), 15 | title: `Hello, world! #${id}`, 16 | content: `blah blah blah `.repeat(20).trim(), 17 | createdAt: new Date(), 18 | }); 19 | } 20 | const encoded = encode(data); 21 | const file = "benchmark/tmp.msgpack"; 22 | writeFileSync(file, encoded); 23 | process.on("exit", () => unlinkSync(file)); 24 | console.log(`encoded size ${Math.round(encoded.byteLength / 1024)}KiB`); 25 | 26 | deepStrictEqual(decode(readFileSync(file)), data); 27 | deepStrictEqual(await decodeAsync(createReadStream(file)), data); 28 | 29 | // sync 30 | console.time("readFileSync |> decode"); 31 | for (let i = 0; i < 100; i++) { 32 | decode(readFileSync(file)); 33 | } 34 | console.timeEnd("readFileSync |> decode"); 35 | 36 | // async 37 | console.time("creteReadStream |> decodeAsync"); 38 | for (let i = 0; i < 100; i++) { 39 | await decodeAsync(createReadStream(file)); 40 | } 41 | console.timeEnd("creteReadStream |> decodeAsync"); 42 | 43 | // asyncArrayStream 44 | 45 | console.time("creteReadStream |> decodeArrayStream"); 46 | for (let i = 0; i < 100; i++) { 47 | for await (let result of decodeArrayStream(createReadStream(file))) { 48 | // console.log(result); 49 | } 50 | } 51 | console.timeEnd("creteReadStream |> decodeArrayStream"); 52 | })(); 53 | -------------------------------------------------------------------------------- /benchmark/timestamp-ext.ts: -------------------------------------------------------------------------------- 1 | import { encode, decode } from "../src"; 2 | 3 | const data = new Array(100).fill(new Date()); 4 | 5 | // warm up 6 | const encoded = encode(data); 7 | decode(encoded); 8 | 9 | // run 10 | 11 | console.time("encode"); 12 | for (let i = 0; i < 10000; i++) { 13 | encode(data); 14 | } 15 | console.timeEnd("encode"); 16 | 17 | console.time("decode"); 18 | for (let i = 0; i < 10000; i++) { 19 | decode(encoded); 20 | } 21 | console.timeEnd("decode"); 22 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 90% 6 | threshold: 1% 7 | patch: off 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; 5 | import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin"; 6 | import tsdoc from "eslint-plugin-tsdoc"; 7 | import tsParser from "@typescript-eslint/parser"; 8 | import js from "@eslint/js"; 9 | import { FlatCompat } from "@eslint/eslintrc"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | const compat = new FlatCompat({ 14 | baseDirectory: __dirname, 15 | recommendedConfig: js.configs.recommended, 16 | allConfig: js.configs.all, 17 | }); 18 | 19 | export default [ 20 | { 21 | ignores: ["**/*.js", "test/deno*", "test/bun*"], 22 | }, 23 | ...fixupConfigRules( 24 | compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/recommended", 27 | "plugin:import/recommended", 28 | "plugin:import/typescript", 29 | "prettier", 30 | ), 31 | ), 32 | { 33 | plugins: { 34 | "@typescript-eslint": fixupPluginRules(typescriptEslintEslintPlugin), 35 | tsdoc, 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 5, 41 | sourceType: "script", 42 | 43 | parserOptions: { 44 | project: "./tsconfig.json", 45 | }, 46 | }, 47 | 48 | settings: {}, 49 | 50 | rules: { 51 | "no-constant-condition": [ 52 | "warn", 53 | { 54 | checkLoops: false, 55 | }, 56 | ], 57 | 58 | "no-useless-escape": "warn", 59 | "no-console": "warn", 60 | "no-var": "warn", 61 | "no-return-await": "warn", 62 | "prefer-const": "warn", 63 | "guard-for-in": "warn", 64 | curly: "warn", 65 | "no-param-reassign": "warn", 66 | "prefer-spread": "warn", 67 | "import/no-unresolved": "off", 68 | "import/no-cycle": "error", 69 | "import/no-default-export": "warn", 70 | "tsdoc/syntax": "warn", 71 | "@typescript-eslint/await-thenable": "warn", 72 | 73 | "@typescript-eslint/array-type": [ 74 | "warn", 75 | { 76 | default: "generic", 77 | }, 78 | ], 79 | 80 | "@typescript-eslint/naming-convention": [ 81 | "warn", 82 | { 83 | selector: "default", 84 | format: ["camelCase", "UPPER_CASE", "PascalCase"], 85 | leadingUnderscore: "allow", 86 | }, 87 | { 88 | selector: "typeLike", 89 | format: ["PascalCase"], 90 | leadingUnderscore: "allow", 91 | }, 92 | ], 93 | 94 | "@typescript-eslint/restrict-plus-operands": "warn", 95 | //"@typescript-eslint/no-throw-literal": "warn", 96 | "@typescript-eslint/unbound-method": "warn", 97 | "@typescript-eslint/explicit-module-boundary-types": "warn", 98 | //"@typescript-eslint/no-extra-semi": "warn", 99 | "@typescript-eslint/no-extra-non-null-assertion": "warn", 100 | 101 | "@typescript-eslint/no-unused-vars": [ 102 | "warn", 103 | { 104 | argsIgnorePattern: "^_", 105 | }, 106 | ], 107 | 108 | "@typescript-eslint/no-use-before-define": "warn", 109 | "@typescript-eslint/no-for-in-array": "warn", 110 | "@typescript-eslint/no-unsafe-argument": "warn", 111 | "@typescript-eslint/no-unsafe-call": "warn", 112 | 113 | "@typescript-eslint/no-unnecessary-condition": [ 114 | "warn", 115 | { 116 | allowConstantLoopConditions: true, 117 | }, 118 | ], 119 | 120 | "@typescript-eslint/no-unnecessary-type-constraint": "warn", 121 | "@typescript-eslint/no-implied-eval": "warn", 122 | "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", 123 | "@typescript-eslint/no-invalid-void-type": "warn", 124 | "@typescript-eslint/no-loss-of-precision": "warn", 125 | "@typescript-eslint/no-confusing-void-expression": "warn", 126 | "@typescript-eslint/no-redundant-type-constituents": "warn", 127 | "@typescript-eslint/prefer-for-of": "warn", 128 | "@typescript-eslint/prefer-includes": "warn", 129 | "@typescript-eslint/prefer-string-starts-ends-with": "warn", 130 | "@typescript-eslint/prefer-readonly": "warn", 131 | "@typescript-eslint/prefer-regexp-exec": "warn", 132 | "@typescript-eslint/prefer-nullish-coalescing": "warn", 133 | "@typescript-eslint/prefer-optional-chain": "warn", 134 | "@typescript-eslint/prefer-ts-expect-error": "warn", 135 | "@typescript-eslint/consistent-type-imports": [ 136 | "error", 137 | { 138 | prefer: "type-imports", 139 | disallowTypeAnnotations: false, 140 | }, 141 | ], 142 | "@typescript-eslint/indent": "off", 143 | "@typescript-eslint/no-explicit-any": "off", 144 | "@typescript-eslint/no-empty-interface": "off", 145 | "@typescript-eslint/no-empty-function": "off", 146 | "@typescript-eslint/no-var-requires": "off", 147 | "@typescript-eslint/no-non-null-assertion": "off", 148 | "@typescript-eslint/ban-ts-comment": "off", 149 | }, 150 | }, 151 | ]; 152 | -------------------------------------------------------------------------------- /example/deno-with-esmsh.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno run 2 | /* eslint-disable no-console */ 3 | import * as msgpack from "https://esm.sh/@msgpack/msgpack/mod.ts"; 4 | 5 | console.log(msgpack.decode(msgpack.encode("Hello, world!"))); 6 | -------------------------------------------------------------------------------- /example/deno-with-jsdeliver.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno run 2 | /* eslint-disable no-console */ 3 | import * as msgpack from "https://cdn.jsdelivr.net/npm/@msgpack/msgpack/mod.ts"; 4 | 5 | console.log(msgpack.decode(msgpack.encode("Hello, world!"))); 6 | -------------------------------------------------------------------------------- /example/deno-with-npm.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno run 2 | /* eslint-disable no-console */ 3 | import * as msgpack from "npm:@msgpack/msgpack"; 4 | 5 | console.log(msgpack.decode(msgpack.encode("Hello, world!"))); 6 | -------------------------------------------------------------------------------- /example/deno-with-unpkg.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno run 2 | /* eslint-disable no-console */ 3 | import * as msgpack from "https://unpkg.com/@msgpack/msgpack/mod.ts"; 4 | 5 | console.log(msgpack.decode(msgpack.encode("Hello, world!"))); 6 | -------------------------------------------------------------------------------- /example/fetch-example-server.ts: -------------------------------------------------------------------------------- 1 | // ts-node example/fetch-example-server.ts 2 | // open example/fetch-example.html 3 | 4 | import http from "http"; 5 | import { encode } from "../src"; 6 | 7 | const hostname = "127.0.0.1"; 8 | const port = 8080; 9 | 10 | function bufferView(b: Uint8Array) { 11 | return Buffer.from(b.buffer, b.byteOffset, b.byteLength); 12 | } 13 | 14 | const server = http.createServer((req, res) => { 15 | console.log("accept:", req.method, req.url); 16 | 17 | res.statusCode = 200; 18 | res.setHeader("content-type", "application/x-msgpack"); 19 | res.setHeader("access-control-allow-origin", "*"); 20 | res.end( 21 | bufferView( 22 | encode({ 23 | message: "Hello, world!", 24 | }), 25 | ), 26 | ); 27 | }); 28 | 29 | server.listen(port, hostname, () => { 30 | console.log(`Server running at http://${hostname}:${port}/`); 31 | }); 32 | -------------------------------------------------------------------------------- /example/fetch-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 |
17 |

Fetch API example

18 |

Open DevTool and see the console logs.

19 | 51 |
52 | 59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /example/umd-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 |
14 |

UMD for @msgpack/msgpack

15 |
<script src="https://unpkg.com/@msgpack/msgpack"></script>
16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /example/umd-example.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | "use strict"; 3 | 4 | try { 5 | const object = { 6 | nil: null, 7 | integer: 1, 8 | float: Math.PI, 9 | string: "Hello, world!", 10 | binary: Uint8Array.from([1, 2, 3]), 11 | array: [10, 20, 30], 12 | map: { foo: "bar" }, 13 | timestampExt: new Date(), 14 | }; 15 | 16 | document.writeln("

input:

"); 17 | document.writeln(`
${JSON.stringify(object, undefined, 2)}
`); 18 | 19 | const encoded = MessagePack.encode(object); 20 | 21 | document.writeln("

output:

"); 22 | document.writeln(`
${JSON.stringify(MessagePack.decode(encoded), undefined, 2)}
`); 23 | } catch (e) { 24 | console.error(e); 25 | document.write(`

${e.constructor.name}: ${e.message}

`); 26 | } 27 | -------------------------------------------------------------------------------- /example/webpack-example/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /example/webpack-example/README.md: -------------------------------------------------------------------------------- 1 | # Webpack Example for @msgpack/msgpack 2 | 3 | This example demonstrates tree-shaking with webpack. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | npm install 9 | npx webpack 10 | ls -lh dist/ 11 | ``` 12 | -------------------------------------------------------------------------------- /example/webpack-example/index.ts: -------------------------------------------------------------------------------- 1 | import { encode } from "@msgpack/msgpack"; 2 | 3 | console.log(encode(null)); 4 | 5 | -------------------------------------------------------------------------------- /example/webpack-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@msgpack/msgpack": "../../" 13 | }, 14 | "devDependencies": { 15 | "lodash": "^4.17.20", 16 | "ts-loader": "^8.0.4", 17 | "ts-node": "^9.0.0", 18 | "typescript": "^4.0.3", 19 | "webpack": "^4.44.2", 20 | "webpack-cli": "^3.3.12" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/webpack-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | "paths": { 47 | "@msgpack/msgpack": ["../../"] 48 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /example/webpack-example/webpack.config.ts: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const _ = require("lodash"); 4 | 5 | const config = { 6 | mode: "production", 7 | 8 | entry: "./index.ts", 9 | output: { 10 | path: path.resolve(__dirname, "dist"), 11 | filename: undefined, // will be set later 12 | }, 13 | resolve: { 14 | extensions: [".ts", ".tsx", ".mjs", ".js", ".json", ".wasm"], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | loader: "ts-loader", 21 | options: { 22 | configFile: "tsconfig.json", 23 | }, 24 | }, 25 | ], 26 | }, 27 | 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | // eslint-disable-next-line @typescript-eslint/naming-convention 31 | "process.env.TEXT_ENCODING": "undefined", 32 | // eslint-disable-next-line @typescript-eslint/naming-convention 33 | "process.env.TEXT_DECODER": "undefined", 34 | }), 35 | ], 36 | 37 | optimization: { 38 | noEmitOnErrors: true, 39 | minimize: false, 40 | }, 41 | 42 | // We don't need NodeJS stuff on browsers! 43 | // https://webpack.js.org/configuration/node/ 44 | node: false, 45 | 46 | devtool: "source-map", 47 | }; 48 | 49 | module.exports = [ 50 | ((config) => { 51 | config.output.filename = "bundle.min.js"; 52 | config.optimization.minimize = true; 53 | return config; 54 | })(_.cloneDeep(config)), 55 | 56 | ((config) => { 57 | config.output.filename = "bundle.js"; 58 | config.optimization.minimize = false; 59 | return config; 60 | })(_.cloneDeep(config)), 61 | ]; 62 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | // const webpack = require("webpack"); 2 | 3 | // eslint-disable-next-line import/no-default-export 4 | export default function configure(config: any) { 5 | config.set({ 6 | customLaunchers: { 7 | // To debug it wih IE11, 8 | // Install `karma-virtualbox-ie11-launcher`, 9 | // and configure custom launchers like this: 10 | // IE11: { 11 | // base: "VirtualBoxIE11", 12 | // keepAlive: true, 13 | // vmName: "IE11 - Win10", 14 | // }, 15 | }, 16 | browsers: ["ChromeHeadless", "FirefoxHeadless"], 17 | 18 | basePath: "", 19 | frameworks: ["mocha"], 20 | files: ["./test/karma-run.ts"], 21 | exclude: [], 22 | preprocessors: { 23 | "**/*.ts": ["webpack", "sourcemap"], 24 | }, 25 | reporters: ["dots"], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | singleRun: false, 31 | concurrency: 1, 32 | browserNoActivityTimeout: 60_000, 33 | 34 | webpack: { 35 | mode: "production", 36 | 37 | resolve: { 38 | extensions: [".ts", ".tsx", ".mjs", ".js", ".json", ".wasm"], 39 | }, 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.tsx?$/, 44 | loader: "ts-loader", 45 | options: { 46 | transpileOnly: true, 47 | configFile: "tsconfig.test-karma.json", 48 | }, 49 | }, 50 | ], 51 | }, 52 | plugins: [], 53 | optimization: { 54 | minimize: false, 55 | }, 56 | performance: { 57 | hints: false, 58 | }, 59 | devtool: "inline-source-map", 60 | }, 61 | mime: { 62 | "text/x-typescript": ["ts", "tsx"], 63 | }, 64 | client: { 65 | mocha: { 66 | timeout: 15_000, 67 | }, 68 | }, 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist.esm/index.mjs"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@msgpack/msgpack", 3 | "version": "3.1.2", 4 | "description": "MessagePack for ECMA-262/JavaScript/TypeScript", 5 | "author": "The MessagePack community", 6 | "license": "ISC", 7 | "main": "./dist.cjs/index.cjs", 8 | "module": "./dist.esm/index.mjs", 9 | "cdn": "./dist.umd/msgpack.min.js", 10 | "unpkg": "./dist.umd/msgpack.min.js", 11 | "types": "./dist.esm/index.d.ts", 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "npm publish --dry-run", 15 | "prepare": "npm run clean && webpack --bail && tsc --build tsconfig.dist.cjs.json tsconfig.dist.esm.json && tsimp tools/fix-ext.mts --mjs dist.esm/*.js dist.esm/*/*.js && tsimp tools/fix-ext.mts --cjs dist.cjs/*.js dist.cjs/*/*.js", 16 | "prepublishOnly": "npm run test:dist", 17 | "clean": "rimraf build dist dist.*", 18 | "test": "mocha 'test/**/*.test.ts'", 19 | "test:dist": "npm run lint && npm run test && npm run test:deno", 20 | "test:cover": "npm run cover:clean && npx nyc --no-clean npm run 'test' && npm run cover:report", 21 | "test:node_with_strip_types": "node --experimental-strip-types test/deno_test.ts", 22 | "test:deno": "deno test --allow-read test/deno_*.ts", 23 | "test:bun": "bun test test/bun.spec.ts", 24 | "test:fuzz": "npm exec --yes -- jsfuzz@git+https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz.git#39e6cf16613a0e30c7a7953f62e64292dbd5d3f3 --fuzzTime 60 --no-versifier test/decode.jsfuzz.js corpus", 25 | "cover:clean": "rimraf .nyc_output coverage/", 26 | "cover:report": "npx nyc report --reporter=text-summary --reporter=html --reporter=json", 27 | "test:browser": "karma start --single-run", 28 | "test:browser:firefox": "karma start --single-run --browsers FirefoxHeadless", 29 | "test:browser:chrome": "karma start --single-run --browsers ChromeHeadless", 30 | "test:watch:browser": "karma start --browsers ChromeHeadless,FirefoxHeadless", 31 | "test:watch:nodejs": "mocha -w 'test/**/*.test.ts'", 32 | "lint": "eslint src test", 33 | "lint:fix": "prettier --loglevel=warn --write 'src/**/*.ts' 'test/**/*.ts' && eslint --fix --ext .ts src test", 34 | "lint:print-config": "eslint --print-config .eslintrc.js", 35 | "update-dependencies": "npx rimraf node_modules/ package-lock.json ; npm install ; npm audit fix --force ; git restore package.json ; npm install" 36 | }, 37 | "homepage": "https://msgpack.org/", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/msgpack/msgpack-javascript.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/msgpack/msgpack-javascript/issues" 44 | }, 45 | "keywords": [ 46 | "msgpack", 47 | "MessagePack", 48 | "serialization", 49 | "universal" 50 | ], 51 | "engines": { 52 | "node": ">= 18" 53 | }, 54 | "devDependencies": { 55 | "@eslint/compat": "latest", 56 | "@eslint/eslintrc": "latest", 57 | "@eslint/js": "latest", 58 | "@types/lodash": "latest", 59 | "@types/mocha": "latest", 60 | "@types/node": "latest", 61 | "@typescript-eslint/eslint-plugin": "latest", 62 | "@typescript-eslint/parser": "latest", 63 | "assert": "latest", 64 | "benchmark": "latest", 65 | "buffer": "latest", 66 | "core-js": "latest", 67 | "eslint": "latest", 68 | "eslint-config-prettier": "latest", 69 | "eslint-plugin-import": "latest", 70 | "eslint-plugin-tsdoc": "latest", 71 | "ieee754": "latest", 72 | "karma": "latest", 73 | "karma-chrome-launcher": "latest", 74 | "karma-cli": "latest", 75 | "karma-firefox-launcher": "latest", 76 | "karma-mocha": "latest", 77 | "karma-sourcemap-loader": "latest", 78 | "karma-webpack": "latest", 79 | "lodash": "latest", 80 | "mocha": "latest", 81 | "msg-timestamp": "latest", 82 | "msgpack-test-js": "latest", 83 | "prettier": "latest", 84 | "rimraf": "latest", 85 | "ts-loader": "latest", 86 | "ts-node": "latest", 87 | "tsimp": "latest", 88 | "typescript": "latest", 89 | "webpack": "latest", 90 | "webpack-cli": "latest" 91 | }, 92 | "files": [ 93 | "src/**/*.*", 94 | "dist.cjs/**/*.*", 95 | "dist.esm/**/*.*", 96 | "dist.umd/**/*.*", 97 | "mod.ts" 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // https://prettier.io/docs/en/options.html 2 | 3 | module.exports = { 4 | printWidth: 120, 5 | trailingComma: "all", 6 | quoteProps: "preserve", 7 | }; 8 | -------------------------------------------------------------------------------- /src/CachedKeyDecoder.ts: -------------------------------------------------------------------------------- 1 | import { utf8DecodeJs } from "./utils/utf8.ts"; 2 | 3 | const DEFAULT_MAX_KEY_LENGTH = 16; 4 | const DEFAULT_MAX_LENGTH_PER_KEY = 16; 5 | 6 | export interface KeyDecoder { 7 | canBeCached(byteLength: number): boolean; 8 | decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string; 9 | } 10 | interface KeyCacheRecord { 11 | readonly bytes: Uint8Array; 12 | readonly str: string; 13 | } 14 | 15 | export class CachedKeyDecoder implements KeyDecoder { 16 | hit = 0; 17 | miss = 0; 18 | private readonly caches: Array>; 19 | readonly maxKeyLength: number; 20 | readonly maxLengthPerKey: number; 21 | 22 | constructor(maxKeyLength = DEFAULT_MAX_KEY_LENGTH, maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY) { 23 | this.maxKeyLength = maxKeyLength; 24 | this.maxLengthPerKey = maxLengthPerKey; 25 | 26 | // avoid `new Array(N)`, which makes a sparse array, 27 | // because a sparse array is typically slower than a non-sparse array. 28 | this.caches = []; 29 | for (let i = 0; i < this.maxKeyLength; i++) { 30 | this.caches.push([]); 31 | } 32 | } 33 | 34 | public canBeCached(byteLength: number): boolean { 35 | return byteLength > 0 && byteLength <= this.maxKeyLength; 36 | } 37 | 38 | private find(bytes: Uint8Array, inputOffset: number, byteLength: number): string | null { 39 | const records = this.caches[byteLength - 1]!; 40 | 41 | FIND_CHUNK: for (const record of records) { 42 | const recordBytes = record.bytes; 43 | 44 | for (let j = 0; j < byteLength; j++) { 45 | if (recordBytes[j] !== bytes[inputOffset + j]) { 46 | continue FIND_CHUNK; 47 | } 48 | } 49 | return record.str; 50 | } 51 | return null; 52 | } 53 | 54 | private store(bytes: Uint8Array, value: string) { 55 | const records = this.caches[bytes.length - 1]!; 56 | const record: KeyCacheRecord = { bytes, str: value }; 57 | 58 | if (records.length >= this.maxLengthPerKey) { 59 | // `records` are full! 60 | // Set `record` to an arbitrary position. 61 | records[(Math.random() * records.length) | 0] = record; 62 | } else { 63 | records.push(record); 64 | } 65 | } 66 | 67 | public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string { 68 | const cachedValue = this.find(bytes, inputOffset, byteLength); 69 | if (cachedValue != null) { 70 | this.hit++; 71 | return cachedValue; 72 | } 73 | this.miss++; 74 | 75 | const str = utf8DecodeJs(bytes, inputOffset, byteLength); 76 | // Ensure to copy a slice of bytes because the bytes may be a NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer. 77 | const slicedCopyOfBytes = Uint8Array.prototype.slice.call(bytes, inputOffset, inputOffset + byteLength); 78 | this.store(slicedCopyOfBytes, str); 79 | return str; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/DecodeError.ts: -------------------------------------------------------------------------------- 1 | export class DecodeError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | 5 | // fix the prototype chain in a cross-platform way 6 | const proto: typeof DecodeError.prototype = Object.create(DecodeError.prototype); 7 | Object.setPrototypeOf(this, proto); 8 | 9 | Object.defineProperty(this, "name", { 10 | configurable: true, 11 | enumerable: false, 12 | value: DecodeError.name, 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Encoder.ts: -------------------------------------------------------------------------------- 1 | import { utf8Count, utf8Encode } from "./utils/utf8.ts"; 2 | import { ExtensionCodec } from "./ExtensionCodec.ts"; 3 | import { setInt64, setUint64 } from "./utils/int.ts"; 4 | import { ensureUint8Array } from "./utils/typedArrays.ts"; 5 | import type { ExtData } from "./ExtData.ts"; 6 | import type { ContextOf } from "./context.ts"; 7 | import type { ExtensionCodecType } from "./ExtensionCodec.ts"; 8 | 9 | export const DEFAULT_MAX_DEPTH = 100; 10 | export const DEFAULT_INITIAL_BUFFER_SIZE = 2048; 11 | 12 | export type EncoderOptions = Partial< 13 | Readonly<{ 14 | extensionCodec: ExtensionCodecType; 15 | 16 | /** 17 | * Encodes bigint as Int64 or Uint64 if it's set to true. 18 | * {@link forceIntegerToFloat} does not affect bigint. 19 | * Depends on ES2020's {@link DataView#setBigInt64} and 20 | * {@link DataView#setBigUint64}. 21 | * 22 | * Defaults to false. 23 | */ 24 | useBigInt64: boolean; 25 | 26 | /** 27 | * The maximum depth in nested objects and arrays. 28 | * 29 | * Defaults to 100. 30 | */ 31 | maxDepth: number; 32 | 33 | /** 34 | * The initial size of the internal buffer. 35 | * 36 | * Defaults to 2048. 37 | */ 38 | initialBufferSize: number; 39 | 40 | /** 41 | * If `true`, the keys of an object is sorted. In other words, the encoded 42 | * binary is canonical and thus comparable to another encoded binary. 43 | * 44 | * Defaults to `false`. If enabled, it spends more time in encoding objects. 45 | */ 46 | sortKeys: boolean; 47 | /** 48 | * If `true`, non-integer numbers are encoded in float32, not in float64 (the default). 49 | * 50 | * Only use it if precisions don't matter. 51 | * 52 | * Defaults to `false`. 53 | */ 54 | forceFloat32: boolean; 55 | 56 | /** 57 | * If `true`, an object property with `undefined` value are ignored. 58 | * e.g. `{ foo: undefined }` will be encoded as `{}`, as `JSON.stringify()` does. 59 | * 60 | * Defaults to `false`. If enabled, it spends more time in encoding objects. 61 | */ 62 | ignoreUndefined: boolean; 63 | 64 | /** 65 | * If `true`, integer numbers are encoded as floating point numbers, 66 | * with the `forceFloat32` option taken into account. 67 | * 68 | * Defaults to `false`. 69 | */ 70 | forceIntegerToFloat: boolean; 71 | }> 72 | > & 73 | ContextOf; 74 | 75 | export class Encoder { 76 | private readonly extensionCodec: ExtensionCodecType; 77 | private readonly context: ContextType; 78 | private readonly useBigInt64: boolean; 79 | private readonly maxDepth: number; 80 | private readonly initialBufferSize: number; 81 | private readonly sortKeys: boolean; 82 | private readonly forceFloat32: boolean; 83 | private readonly ignoreUndefined: boolean; 84 | private readonly forceIntegerToFloat: boolean; 85 | 86 | private pos: number; 87 | private view: DataView; 88 | private bytes: Uint8Array; 89 | 90 | private entered = false; 91 | 92 | public constructor(options?: EncoderOptions) { 93 | this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType); 94 | this.context = (options as { context: ContextType } | undefined)?.context as ContextType; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined 95 | 96 | this.useBigInt64 = options?.useBigInt64 ?? false; 97 | this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH; 98 | this.initialBufferSize = options?.initialBufferSize ?? DEFAULT_INITIAL_BUFFER_SIZE; 99 | this.sortKeys = options?.sortKeys ?? false; 100 | this.forceFloat32 = options?.forceFloat32 ?? false; 101 | this.ignoreUndefined = options?.ignoreUndefined ?? false; 102 | this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false; 103 | 104 | this.pos = 0; 105 | this.view = new DataView(new ArrayBuffer(this.initialBufferSize)); 106 | this.bytes = new Uint8Array(this.view.buffer); 107 | } 108 | 109 | private clone() { 110 | // Because of slightly special argument `context`, 111 | // type assertion is needed. 112 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 113 | return new Encoder({ 114 | extensionCodec: this.extensionCodec, 115 | context: this.context, 116 | useBigInt64: this.useBigInt64, 117 | maxDepth: this.maxDepth, 118 | initialBufferSize: this.initialBufferSize, 119 | sortKeys: this.sortKeys, 120 | forceFloat32: this.forceFloat32, 121 | ignoreUndefined: this.ignoreUndefined, 122 | forceIntegerToFloat: this.forceIntegerToFloat, 123 | } as any); 124 | } 125 | 126 | private reinitializeState() { 127 | this.pos = 0; 128 | } 129 | 130 | /** 131 | * This is almost equivalent to {@link Encoder#encode}, but it returns an reference of the encoder's internal buffer and thus much faster than {@link Encoder#encode}. 132 | * 133 | * @returns Encodes the object and returns a shared reference the encoder's internal buffer. 134 | */ 135 | public encodeSharedRef(object: unknown): Uint8Array { 136 | if (this.entered) { 137 | const instance = this.clone(); 138 | return instance.encodeSharedRef(object); 139 | } 140 | 141 | try { 142 | this.entered = true; 143 | 144 | this.reinitializeState(); 145 | this.doEncode(object, 1); 146 | return this.bytes.subarray(0, this.pos); 147 | } finally { 148 | this.entered = false; 149 | } 150 | } 151 | 152 | /** 153 | * @returns Encodes the object and returns a copy of the encoder's internal buffer. 154 | */ 155 | public encode(object: unknown): Uint8Array { 156 | if (this.entered) { 157 | const instance = this.clone(); 158 | return instance.encode(object); 159 | } 160 | 161 | try { 162 | this.entered = true; 163 | 164 | this.reinitializeState(); 165 | this.doEncode(object, 1); 166 | return this.bytes.slice(0, this.pos); 167 | } finally { 168 | this.entered = false; 169 | } 170 | } 171 | 172 | private doEncode(object: unknown, depth: number): void { 173 | if (depth > this.maxDepth) { 174 | throw new Error(`Too deep objects in depth ${depth}`); 175 | } 176 | 177 | if (object == null) { 178 | this.encodeNil(); 179 | } else if (typeof object === "boolean") { 180 | this.encodeBoolean(object); 181 | } else if (typeof object === "number") { 182 | if (!this.forceIntegerToFloat) { 183 | this.encodeNumber(object); 184 | } else { 185 | this.encodeNumberAsFloat(object); 186 | } 187 | } else if (typeof object === "string") { 188 | this.encodeString(object); 189 | } else if (this.useBigInt64 && typeof object === "bigint") { 190 | this.encodeBigInt64(object); 191 | } else { 192 | this.encodeObject(object, depth); 193 | } 194 | } 195 | 196 | private ensureBufferSizeToWrite(sizeToWrite: number) { 197 | const requiredSize = this.pos + sizeToWrite; 198 | 199 | if (this.view.byteLength < requiredSize) { 200 | this.resizeBuffer(requiredSize * 2); 201 | } 202 | } 203 | 204 | private resizeBuffer(newSize: number) { 205 | const newBuffer = new ArrayBuffer(newSize); 206 | const newBytes = new Uint8Array(newBuffer); 207 | const newView = new DataView(newBuffer); 208 | 209 | newBytes.set(this.bytes); 210 | 211 | this.view = newView; 212 | this.bytes = newBytes; 213 | } 214 | 215 | private encodeNil() { 216 | this.writeU8(0xc0); 217 | } 218 | 219 | private encodeBoolean(object: boolean) { 220 | if (object === false) { 221 | this.writeU8(0xc2); 222 | } else { 223 | this.writeU8(0xc3); 224 | } 225 | } 226 | 227 | private encodeNumber(object: number): void { 228 | if (!this.forceIntegerToFloat && Number.isSafeInteger(object)) { 229 | if (object >= 0) { 230 | if (object < 0x80) { 231 | // positive fixint 232 | this.writeU8(object); 233 | } else if (object < 0x100) { 234 | // uint 8 235 | this.writeU8(0xcc); 236 | this.writeU8(object); 237 | } else if (object < 0x10000) { 238 | // uint 16 239 | this.writeU8(0xcd); 240 | this.writeU16(object); 241 | } else if (object < 0x100000000) { 242 | // uint 32 243 | this.writeU8(0xce); 244 | this.writeU32(object); 245 | } else if (!this.useBigInt64) { 246 | // uint 64 247 | this.writeU8(0xcf); 248 | this.writeU64(object); 249 | } else { 250 | this.encodeNumberAsFloat(object); 251 | } 252 | } else { 253 | if (object >= -0x20) { 254 | // negative fixint 255 | this.writeU8(0xe0 | (object + 0x20)); 256 | } else if (object >= -0x80) { 257 | // int 8 258 | this.writeU8(0xd0); 259 | this.writeI8(object); 260 | } else if (object >= -0x8000) { 261 | // int 16 262 | this.writeU8(0xd1); 263 | this.writeI16(object); 264 | } else if (object >= -0x80000000) { 265 | // int 32 266 | this.writeU8(0xd2); 267 | this.writeI32(object); 268 | } else if (!this.useBigInt64) { 269 | // int 64 270 | this.writeU8(0xd3); 271 | this.writeI64(object); 272 | } else { 273 | this.encodeNumberAsFloat(object); 274 | } 275 | } 276 | } else { 277 | this.encodeNumberAsFloat(object); 278 | } 279 | } 280 | 281 | private encodeNumberAsFloat(object: number): void { 282 | if (this.forceFloat32) { 283 | // float 32 284 | this.writeU8(0xca); 285 | this.writeF32(object); 286 | } else { 287 | // float 64 288 | this.writeU8(0xcb); 289 | this.writeF64(object); 290 | } 291 | } 292 | 293 | private encodeBigInt64(object: bigint): void { 294 | if (object >= BigInt(0)) { 295 | // uint 64 296 | this.writeU8(0xcf); 297 | this.writeBigUint64(object); 298 | } else { 299 | // int 64 300 | this.writeU8(0xd3); 301 | this.writeBigInt64(object); 302 | } 303 | } 304 | 305 | private writeStringHeader(byteLength: number) { 306 | if (byteLength < 32) { 307 | // fixstr 308 | this.writeU8(0xa0 + byteLength); 309 | } else if (byteLength < 0x100) { 310 | // str 8 311 | this.writeU8(0xd9); 312 | this.writeU8(byteLength); 313 | } else if (byteLength < 0x10000) { 314 | // str 16 315 | this.writeU8(0xda); 316 | this.writeU16(byteLength); 317 | } else if (byteLength < 0x100000000) { 318 | // str 32 319 | this.writeU8(0xdb); 320 | this.writeU32(byteLength); 321 | } else { 322 | throw new Error(`Too long string: ${byteLength} bytes in UTF-8`); 323 | } 324 | } 325 | 326 | private encodeString(object: string) { 327 | const maxHeaderSize = 1 + 4; 328 | 329 | const byteLength = utf8Count(object); 330 | this.ensureBufferSizeToWrite(maxHeaderSize + byteLength); 331 | this.writeStringHeader(byteLength); 332 | utf8Encode(object, this.bytes, this.pos); 333 | this.pos += byteLength; 334 | } 335 | 336 | private encodeObject(object: unknown, depth: number) { 337 | // try to encode objects with custom codec first of non-primitives 338 | const ext = this.extensionCodec.tryToEncode(object, this.context); 339 | if (ext != null) { 340 | this.encodeExtension(ext); 341 | } else if (Array.isArray(object)) { 342 | this.encodeArray(object, depth); 343 | } else if (ArrayBuffer.isView(object)) { 344 | this.encodeBinary(object); 345 | } else if (typeof object === "object") { 346 | this.encodeMap(object as Record, depth); 347 | } else { 348 | // symbol, function and other special object come here unless extensionCodec handles them. 349 | throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(object)}`); 350 | } 351 | } 352 | 353 | private encodeBinary(object: ArrayBufferView) { 354 | const size = object.byteLength; 355 | if (size < 0x100) { 356 | // bin 8 357 | this.writeU8(0xc4); 358 | this.writeU8(size); 359 | } else if (size < 0x10000) { 360 | // bin 16 361 | this.writeU8(0xc5); 362 | this.writeU16(size); 363 | } else if (size < 0x100000000) { 364 | // bin 32 365 | this.writeU8(0xc6); 366 | this.writeU32(size); 367 | } else { 368 | throw new Error(`Too large binary: ${size}`); 369 | } 370 | const bytes = ensureUint8Array(object); 371 | this.writeU8a(bytes); 372 | } 373 | 374 | private encodeArray(object: Array, depth: number) { 375 | const size = object.length; 376 | if (size < 16) { 377 | // fixarray 378 | this.writeU8(0x90 + size); 379 | } else if (size < 0x10000) { 380 | // array 16 381 | this.writeU8(0xdc); 382 | this.writeU16(size); 383 | } else if (size < 0x100000000) { 384 | // array 32 385 | this.writeU8(0xdd); 386 | this.writeU32(size); 387 | } else { 388 | throw new Error(`Too large array: ${size}`); 389 | } 390 | for (const item of object) { 391 | this.doEncode(item, depth + 1); 392 | } 393 | } 394 | 395 | private countWithoutUndefined(object: Record, keys: ReadonlyArray): number { 396 | let count = 0; 397 | 398 | for (const key of keys) { 399 | if (object[key] !== undefined) { 400 | count++; 401 | } 402 | } 403 | 404 | return count; 405 | } 406 | 407 | private encodeMap(object: Record, depth: number) { 408 | const keys = Object.keys(object); 409 | if (this.sortKeys) { 410 | keys.sort(); 411 | } 412 | 413 | const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length; 414 | 415 | if (size < 16) { 416 | // fixmap 417 | this.writeU8(0x80 + size); 418 | } else if (size < 0x10000) { 419 | // map 16 420 | this.writeU8(0xde); 421 | this.writeU16(size); 422 | } else if (size < 0x100000000) { 423 | // map 32 424 | this.writeU8(0xdf); 425 | this.writeU32(size); 426 | } else { 427 | throw new Error(`Too large map object: ${size}`); 428 | } 429 | 430 | for (const key of keys) { 431 | const value = object[key]; 432 | 433 | if (!(this.ignoreUndefined && value === undefined)) { 434 | this.encodeString(key); 435 | this.doEncode(value, depth + 1); 436 | } 437 | } 438 | } 439 | 440 | private encodeExtension(ext: ExtData) { 441 | if (typeof ext.data === "function") { 442 | const data = ext.data(this.pos + 6); 443 | const size = data.length; 444 | 445 | if (size >= 0x100000000) { 446 | throw new Error(`Too large extension object: ${size}`); 447 | } 448 | 449 | this.writeU8(0xc9); 450 | this.writeU32(size); 451 | this.writeI8(ext.type); 452 | this.writeU8a(data); 453 | return; 454 | } 455 | 456 | const size = ext.data.length; 457 | if (size === 1) { 458 | // fixext 1 459 | this.writeU8(0xd4); 460 | } else if (size === 2) { 461 | // fixext 2 462 | this.writeU8(0xd5); 463 | } else if (size === 4) { 464 | // fixext 4 465 | this.writeU8(0xd6); 466 | } else if (size === 8) { 467 | // fixext 8 468 | this.writeU8(0xd7); 469 | } else if (size === 16) { 470 | // fixext 16 471 | this.writeU8(0xd8); 472 | } else if (size < 0x100) { 473 | // ext 8 474 | this.writeU8(0xc7); 475 | this.writeU8(size); 476 | } else if (size < 0x10000) { 477 | // ext 16 478 | this.writeU8(0xc8); 479 | this.writeU16(size); 480 | } else if (size < 0x100000000) { 481 | // ext 32 482 | this.writeU8(0xc9); 483 | this.writeU32(size); 484 | } else { 485 | throw new Error(`Too large extension object: ${size}`); 486 | } 487 | this.writeI8(ext.type); 488 | this.writeU8a(ext.data); 489 | } 490 | 491 | private writeU8(value: number) { 492 | this.ensureBufferSizeToWrite(1); 493 | 494 | this.view.setUint8(this.pos, value); 495 | this.pos++; 496 | } 497 | 498 | private writeU8a(values: ArrayLike) { 499 | const size = values.length; 500 | this.ensureBufferSizeToWrite(size); 501 | 502 | this.bytes.set(values, this.pos); 503 | this.pos += size; 504 | } 505 | 506 | private writeI8(value: number) { 507 | this.ensureBufferSizeToWrite(1); 508 | 509 | this.view.setInt8(this.pos, value); 510 | this.pos++; 511 | } 512 | 513 | private writeU16(value: number) { 514 | this.ensureBufferSizeToWrite(2); 515 | 516 | this.view.setUint16(this.pos, value); 517 | this.pos += 2; 518 | } 519 | 520 | private writeI16(value: number) { 521 | this.ensureBufferSizeToWrite(2); 522 | 523 | this.view.setInt16(this.pos, value); 524 | this.pos += 2; 525 | } 526 | 527 | private writeU32(value: number) { 528 | this.ensureBufferSizeToWrite(4); 529 | 530 | this.view.setUint32(this.pos, value); 531 | this.pos += 4; 532 | } 533 | 534 | private writeI32(value: number) { 535 | this.ensureBufferSizeToWrite(4); 536 | 537 | this.view.setInt32(this.pos, value); 538 | this.pos += 4; 539 | } 540 | 541 | private writeF32(value: number) { 542 | this.ensureBufferSizeToWrite(4); 543 | 544 | this.view.setFloat32(this.pos, value); 545 | this.pos += 4; 546 | } 547 | 548 | private writeF64(value: number) { 549 | this.ensureBufferSizeToWrite(8); 550 | 551 | this.view.setFloat64(this.pos, value); 552 | this.pos += 8; 553 | } 554 | 555 | private writeU64(value: number) { 556 | this.ensureBufferSizeToWrite(8); 557 | 558 | setUint64(this.view, this.pos, value); 559 | this.pos += 8; 560 | } 561 | 562 | private writeI64(value: number) { 563 | this.ensureBufferSizeToWrite(8); 564 | 565 | setInt64(this.view, this.pos, value); 566 | this.pos += 8; 567 | } 568 | 569 | private writeBigUint64(value: bigint) { 570 | this.ensureBufferSizeToWrite(8); 571 | 572 | this.view.setBigUint64(this.pos, value); 573 | this.pos += 8; 574 | } 575 | 576 | private writeBigInt64(value: bigint) { 577 | this.ensureBufferSizeToWrite(8); 578 | 579 | this.view.setBigInt64(this.pos, value); 580 | this.pos += 8; 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /src/ExtData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ExtData is used to handle Extension Types that are not registered to ExtensionCodec. 3 | */ 4 | export class ExtData { 5 | readonly type: number; 6 | readonly data: Uint8Array | ((pos: number) => Uint8Array); 7 | 8 | constructor(type: number, data: Uint8Array | ((pos: number) => Uint8Array)) { 9 | this.type = type; 10 | this.data = data; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ExtensionCodec.ts: -------------------------------------------------------------------------------- 1 | // ExtensionCodec to handle MessagePack extensions 2 | 3 | import { ExtData } from "./ExtData.ts"; 4 | import { timestampExtension } from "./timestamp.ts"; 5 | 6 | export type ExtensionDecoderType = ( 7 | data: Uint8Array, 8 | extensionType: number, 9 | context: ContextType, 10 | ) => unknown; 11 | 12 | export type ExtensionEncoderType = ( 13 | input: unknown, 14 | context: ContextType, 15 | ) => Uint8Array | ((dataPos: number) => Uint8Array) | null; 16 | 17 | // immutable interface to ExtensionCodec 18 | export type ExtensionCodecType = { 19 | // eslint-disable-next-line @typescript-eslint/naming-convention 20 | __brand?: ContextType; 21 | tryToEncode(object: unknown, context: ContextType): ExtData | null; 22 | decode(data: Uint8Array, extType: number, context: ContextType): unknown; 23 | }; 24 | 25 | export class ExtensionCodec implements ExtensionCodecType { 26 | public static readonly defaultCodec: ExtensionCodecType = new ExtensionCodec(); 27 | 28 | // ensures ExtensionCodecType matches ExtensionCodec 29 | // this will make type errors a lot more clear 30 | // eslint-disable-next-line @typescript-eslint/naming-convention 31 | __brand?: ContextType; 32 | 33 | // built-in extensions 34 | private readonly builtInEncoders: Array | undefined | null> = []; 35 | private readonly builtInDecoders: Array | undefined | null> = []; 36 | 37 | // custom extensions 38 | private readonly encoders: Array | undefined | null> = []; 39 | private readonly decoders: Array | undefined | null> = []; 40 | 41 | public constructor() { 42 | this.register(timestampExtension); 43 | } 44 | 45 | public register({ 46 | type, 47 | encode, 48 | decode, 49 | }: { 50 | type: number; 51 | encode: ExtensionEncoderType; 52 | decode: ExtensionDecoderType; 53 | }): void { 54 | if (type >= 0) { 55 | // custom extensions 56 | this.encoders[type] = encode; 57 | this.decoders[type] = decode; 58 | } else { 59 | // built-in extensions 60 | const index = -1 - type; 61 | this.builtInEncoders[index] = encode; 62 | this.builtInDecoders[index] = decode; 63 | } 64 | } 65 | 66 | public tryToEncode(object: unknown, context: ContextType): ExtData | null { 67 | // built-in extensions 68 | for (let i = 0; i < this.builtInEncoders.length; i++) { 69 | const encodeExt = this.builtInEncoders[i]; 70 | if (encodeExt != null) { 71 | const data = encodeExt(object, context); 72 | if (data != null) { 73 | const type = -1 - i; 74 | return new ExtData(type, data); 75 | } 76 | } 77 | } 78 | 79 | // custom extensions 80 | for (let i = 0; i < this.encoders.length; i++) { 81 | const encodeExt = this.encoders[i]; 82 | if (encodeExt != null) { 83 | const data = encodeExt(object, context); 84 | if (data != null) { 85 | const type = i; 86 | return new ExtData(type, data); 87 | } 88 | } 89 | } 90 | 91 | if (object instanceof ExtData) { 92 | // to keep ExtData as is 93 | return object; 94 | } 95 | return null; 96 | } 97 | 98 | public decode(data: Uint8Array, type: number, context: ContextType): unknown { 99 | const decodeExt = type < 0 ? this.builtInDecoders[-1 - type] : this.decoders[type]; 100 | if (decodeExt) { 101 | return decodeExt(data, type, context); 102 | } else { 103 | // decode() does not fail, returns ExtData instead. 104 | return new ExtData(type, data); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | type SplitTypes = U extends T ? (Exclude extends never ? T : Exclude) : T; 2 | 3 | export type SplitUndefined = SplitTypes; 4 | 5 | export type ContextOf = ContextType extends undefined 6 | ? object 7 | : { 8 | /** 9 | * Custom user-defined data, read/writable 10 | */ 11 | context: ContextType; 12 | }; 13 | -------------------------------------------------------------------------------- /src/decode.ts: -------------------------------------------------------------------------------- 1 | import { Decoder } from "./Decoder.ts"; 2 | import type { DecoderOptions } from "./Decoder.ts"; 3 | import type { SplitUndefined } from "./context.ts"; 4 | 5 | /** 6 | * It decodes a single MessagePack object in a buffer. 7 | * 8 | * This is a synchronous decoding function. 9 | * See other variants for asynchronous decoding: {@link decodeAsync}, {@link decodeMultiStream}, or {@link decodeArrayStream}. 10 | * 11 | * @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty. 12 | * @throws {@link DecodeError} if the buffer contains invalid data. 13 | */ 14 | export function decode( 15 | buffer: ArrayLike | ArrayBufferView | ArrayBufferLike, 16 | options?: DecoderOptions>, 17 | ): unknown { 18 | const decoder = new Decoder(options); 19 | return decoder.decode(buffer); 20 | } 21 | 22 | /** 23 | * It decodes multiple MessagePack objects in a buffer. 24 | * This is corresponding to {@link decodeMultiStream}. 25 | * 26 | * @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty. 27 | * @throws {@link DecodeError} if the buffer contains invalid data. 28 | */ 29 | export function decodeMulti( 30 | buffer: ArrayLike | BufferSource, 31 | options?: DecoderOptions>, 32 | ): Generator { 33 | const decoder = new Decoder(options); 34 | return decoder.decodeMulti(buffer); 35 | } 36 | -------------------------------------------------------------------------------- /src/decodeAsync.ts: -------------------------------------------------------------------------------- 1 | import { Decoder } from "./Decoder.ts"; 2 | import { ensureAsyncIterable } from "./utils/stream.ts"; 3 | import type { DecoderOptions } from "./Decoder.ts"; 4 | import type { ReadableStreamLike } from "./utils/stream.ts"; 5 | import type { SplitUndefined } from "./context.ts"; 6 | 7 | /** 8 | * @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty. 9 | * @throws {@link DecodeError} if the buffer contains invalid data. 10 | */ 11 | export async function decodeAsync( 12 | streamLike: ReadableStreamLike | BufferSource>, 13 | options?: DecoderOptions>, 14 | ): Promise { 15 | const stream = ensureAsyncIterable(streamLike); 16 | const decoder = new Decoder(options); 17 | return decoder.decodeAsync(stream); 18 | } 19 | 20 | /** 21 | * @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty. 22 | * @throws {@link DecodeError} if the buffer contains invalid data. 23 | */ 24 | export function decodeArrayStream( 25 | streamLike: ReadableStreamLike | BufferSource>, 26 | options?: DecoderOptions>, 27 | ): AsyncGenerator { 28 | const stream = ensureAsyncIterable(streamLike); 29 | const decoder = new Decoder(options); 30 | return decoder.decodeArrayStream(stream); 31 | } 32 | 33 | /** 34 | * @throws {@link RangeError} if the buffer is incomplete, including the case where the buffer is empty. 35 | * @throws {@link DecodeError} if the buffer contains invalid data. 36 | */ 37 | export function decodeMultiStream( 38 | streamLike: ReadableStreamLike | BufferSource>, 39 | options?: DecoderOptions>, 40 | ): AsyncGenerator { 41 | const stream = ensureAsyncIterable(streamLike); 42 | const decoder = new Decoder(options); 43 | return decoder.decodeStream(stream); 44 | } 45 | -------------------------------------------------------------------------------- /src/encode.ts: -------------------------------------------------------------------------------- 1 | import { Encoder } from "./Encoder.ts"; 2 | import type { EncoderOptions } from "./Encoder.ts"; 3 | import type { SplitUndefined } from "./context.ts"; 4 | 5 | /** 6 | * It encodes `value` in the MessagePack format and 7 | * returns a byte buffer. 8 | * 9 | * The returned buffer is a slice of a larger `ArrayBuffer`, so you have to use its `#byteOffset` and `#byteLength` in order to convert it to another typed arrays including NodeJS `Buffer`. 10 | */ 11 | export function encode( 12 | value: unknown, 13 | options?: EncoderOptions>, 14 | ): Uint8Array { 15 | const encoder = new Encoder(options); 16 | return encoder.encodeSharedRef(value); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Main Functions: 2 | 3 | import { encode } from "./encode.ts"; 4 | export { encode }; 5 | 6 | import { decode, decodeMulti } from "./decode.ts"; 7 | export { decode, decodeMulti }; 8 | 9 | import { decodeAsync, decodeArrayStream, decodeMultiStream } from "./decodeAsync.ts"; 10 | export { decodeAsync, decodeArrayStream, decodeMultiStream }; 11 | 12 | import { Decoder } from "./Decoder.ts"; 13 | export { Decoder }; 14 | import type { DecoderOptions } from "./Decoder.ts"; 15 | export type { DecoderOptions }; 16 | import { DecodeError } from "./DecodeError.ts"; 17 | export { DecodeError }; 18 | 19 | import { Encoder } from "./Encoder.ts"; 20 | export { Encoder }; 21 | import type { EncoderOptions } from "./Encoder.ts"; 22 | export type { EncoderOptions }; 23 | 24 | // Utilities for Extension Types: 25 | 26 | import { ExtensionCodec } from "./ExtensionCodec.ts"; 27 | export { ExtensionCodec }; 28 | import type { ExtensionCodecType, ExtensionDecoderType, ExtensionEncoderType } from "./ExtensionCodec.ts"; 29 | export type { ExtensionCodecType, ExtensionDecoderType, ExtensionEncoderType }; 30 | import { ExtData } from "./ExtData.ts"; 31 | export { ExtData }; 32 | 33 | import { 34 | EXT_TIMESTAMP, 35 | encodeDateToTimeSpec, 36 | encodeTimeSpecToTimestamp, 37 | decodeTimestampToTimeSpec, 38 | encodeTimestampExtension, 39 | decodeTimestampExtension, 40 | } from "./timestamp.ts"; 41 | export { 42 | EXT_TIMESTAMP, 43 | encodeDateToTimeSpec, 44 | encodeTimeSpecToTimestamp, 45 | decodeTimestampToTimeSpec, 46 | encodeTimestampExtension, 47 | decodeTimestampExtension, 48 | }; 49 | -------------------------------------------------------------------------------- /src/timestamp.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type 2 | import { DecodeError } from "./DecodeError.ts"; 3 | import { getInt64, setInt64 } from "./utils/int.ts"; 4 | 5 | export const EXT_TIMESTAMP = -1; 6 | 7 | export type TimeSpec = { 8 | sec: number; 9 | nsec: number; 10 | }; 11 | 12 | const TIMESTAMP32_MAX_SEC = 0x100000000 - 1; // 32-bit unsigned int 13 | const TIMESTAMP64_MAX_SEC = 0x400000000 - 1; // 34-bit unsigned int 14 | 15 | export function encodeTimeSpecToTimestamp({ sec, nsec }: TimeSpec): Uint8Array { 16 | if (sec >= 0 && nsec >= 0 && sec <= TIMESTAMP64_MAX_SEC) { 17 | // Here sec >= 0 && nsec >= 0 18 | if (nsec === 0 && sec <= TIMESTAMP32_MAX_SEC) { 19 | // timestamp 32 = { sec32 (unsigned) } 20 | const rv = new Uint8Array(4); 21 | const view = new DataView(rv.buffer); 22 | view.setUint32(0, sec); 23 | return rv; 24 | } else { 25 | // timestamp 64 = { nsec30 (unsigned), sec34 (unsigned) } 26 | const secHigh = sec / 0x100000000; 27 | const secLow = sec & 0xffffffff; 28 | const rv = new Uint8Array(8); 29 | const view = new DataView(rv.buffer); 30 | // nsec30 | secHigh2 31 | view.setUint32(0, (nsec << 2) | (secHigh & 0x3)); 32 | // secLow32 33 | view.setUint32(4, secLow); 34 | return rv; 35 | } 36 | } else { 37 | // timestamp 96 = { nsec32 (unsigned), sec64 (signed) } 38 | const rv = new Uint8Array(12); 39 | const view = new DataView(rv.buffer); 40 | view.setUint32(0, nsec); 41 | setInt64(view, 4, sec); 42 | return rv; 43 | } 44 | } 45 | 46 | export function encodeDateToTimeSpec(date: Date): TimeSpec { 47 | const msec = date.getTime(); 48 | const sec = Math.floor(msec / 1e3); 49 | const nsec = (msec - sec * 1e3) * 1e6; 50 | 51 | // Normalizes { sec, nsec } to ensure nsec is unsigned. 52 | const nsecInSec = Math.floor(nsec / 1e9); 53 | return { 54 | sec: sec + nsecInSec, 55 | nsec: nsec - nsecInSec * 1e9, 56 | }; 57 | } 58 | 59 | export function encodeTimestampExtension(object: unknown): Uint8Array | null { 60 | if (object instanceof Date) { 61 | const timeSpec = encodeDateToTimeSpec(object); 62 | return encodeTimeSpecToTimestamp(timeSpec); 63 | } else { 64 | return null; 65 | } 66 | } 67 | 68 | export function decodeTimestampToTimeSpec(data: Uint8Array): TimeSpec { 69 | const view = new DataView(data.buffer, data.byteOffset, data.byteLength); 70 | 71 | // data may be 32, 64, or 96 bits 72 | switch (data.byteLength) { 73 | case 4: { 74 | // timestamp 32 = { sec32 } 75 | const sec = view.getUint32(0); 76 | const nsec = 0; 77 | return { sec, nsec }; 78 | } 79 | case 8: { 80 | // timestamp 64 = { nsec30, sec34 } 81 | const nsec30AndSecHigh2 = view.getUint32(0); 82 | const secLow32 = view.getUint32(4); 83 | const sec = (nsec30AndSecHigh2 & 0x3) * 0x100000000 + secLow32; 84 | const nsec = nsec30AndSecHigh2 >>> 2; 85 | return { sec, nsec }; 86 | } 87 | case 12: { 88 | // timestamp 96 = { nsec32 (unsigned), sec64 (signed) } 89 | 90 | const sec = getInt64(view, 4); 91 | const nsec = view.getUint32(0); 92 | return { sec, nsec }; 93 | } 94 | default: 95 | throw new DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${data.length}`); 96 | } 97 | } 98 | 99 | export function decodeTimestampExtension(data: Uint8Array): Date { 100 | const timeSpec = decodeTimestampToTimeSpec(data); 101 | return new Date(timeSpec.sec * 1e3 + timeSpec.nsec / 1e6); 102 | } 103 | 104 | export const timestampExtension = { 105 | type: EXT_TIMESTAMP, 106 | encode: encodeTimestampExtension, 107 | decode: decodeTimestampExtension, 108 | }; 109 | -------------------------------------------------------------------------------- /src/utils/int.ts: -------------------------------------------------------------------------------- 1 | // Integer Utility 2 | 3 | export const UINT32_MAX = 0xffff_ffff; 4 | 5 | // DataView extension to handle int64 / uint64, 6 | // where the actual range is 53-bits integer (a.k.a. safe integer) 7 | 8 | export function setUint64(view: DataView, offset: number, value: number): void { 9 | const high = value / 0x1_0000_0000; 10 | const low = value; // high bits are truncated by DataView 11 | view.setUint32(offset, high); 12 | view.setUint32(offset + 4, low); 13 | } 14 | 15 | export function setInt64(view: DataView, offset: number, value: number): void { 16 | const high = Math.floor(value / 0x1_0000_0000); 17 | const low = value; // high bits are truncated by DataView 18 | view.setUint32(offset, high); 19 | view.setUint32(offset + 4, low); 20 | } 21 | 22 | export function getInt64(view: DataView, offset: number): number { 23 | const high = view.getInt32(offset); 24 | const low = view.getUint32(offset + 4); 25 | return high * 0x1_0000_0000 + low; 26 | } 27 | 28 | export function getUint64(view: DataView, offset: number): number { 29 | const high = view.getUint32(offset); 30 | const low = view.getUint32(offset + 4); 31 | return high * 0x1_0000_0000 + low; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/prettyByte.ts: -------------------------------------------------------------------------------- 1 | export function prettyByte(byte: number): string { 2 | return `${byte < 0 ? "-" : ""}0x${Math.abs(byte).toString(16).padStart(2, "0")}`; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/stream.ts: -------------------------------------------------------------------------------- 1 | // utility for whatwg streams 2 | 3 | // The living standard of whatwg streams says 4 | // ReadableStream is also AsyncIterable, but 5 | // as of June 2019, no browser implements it. 6 | // See https://streams.spec.whatwg.org/ for details 7 | export type ReadableStreamLike = AsyncIterable | ReadableStream; 8 | 9 | export function isAsyncIterable(object: ReadableStreamLike): object is AsyncIterable { 10 | return (object as any)[Symbol.asyncIterator] != null; 11 | } 12 | 13 | export async function* asyncIterableFromStream(stream: ReadableStream): AsyncIterable { 14 | const reader = stream.getReader(); 15 | 16 | try { 17 | while (true) { 18 | const { done, value } = await reader.read(); 19 | if (done) { 20 | return; 21 | } 22 | yield value; 23 | } 24 | } finally { 25 | reader.releaseLock(); 26 | } 27 | } 28 | 29 | export function ensureAsyncIterable(streamLike: ReadableStreamLike): AsyncIterable { 30 | if (isAsyncIterable(streamLike)) { 31 | return streamLike; 32 | } else { 33 | return asyncIterableFromStream(streamLike); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/typedArrays.ts: -------------------------------------------------------------------------------- 1 | function isArrayBufferLike(buffer: unknown): buffer is ArrayBufferLike { 2 | return ( 3 | buffer instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer) 4 | ); 5 | } 6 | 7 | export function ensureUint8Array( 8 | buffer: ArrayLike | Uint8Array | ArrayBufferView | ArrayBufferLike, 9 | ): Uint8Array { 10 | if (buffer instanceof Uint8Array) { 11 | return buffer; 12 | } else if (ArrayBuffer.isView(buffer)) { 13 | return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); 14 | } else if (isArrayBufferLike(buffer)) { 15 | return new Uint8Array(buffer); 16 | } else { 17 | // ArrayLike 18 | return Uint8Array.from(buffer); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/utf8.ts: -------------------------------------------------------------------------------- 1 | export function utf8Count(str: string): number { 2 | const strLength = str.length; 3 | 4 | let byteLength = 0; 5 | let pos = 0; 6 | while (pos < strLength) { 7 | let value = str.charCodeAt(pos++); 8 | 9 | if ((value & 0xffffff80) === 0) { 10 | // 1-byte 11 | byteLength++; 12 | continue; 13 | } else if ((value & 0xfffff800) === 0) { 14 | // 2-bytes 15 | byteLength += 2; 16 | } else { 17 | // handle surrogate pair 18 | if (value >= 0xd800 && value <= 0xdbff) { 19 | // high surrogate 20 | if (pos < strLength) { 21 | const extra = str.charCodeAt(pos); 22 | if ((extra & 0xfc00) === 0xdc00) { 23 | ++pos; 24 | value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; 25 | } 26 | } 27 | } 28 | 29 | if ((value & 0xffff0000) === 0) { 30 | // 3-byte 31 | byteLength += 3; 32 | } else { 33 | // 4-byte 34 | byteLength += 4; 35 | } 36 | } 37 | } 38 | return byteLength; 39 | } 40 | 41 | export function utf8EncodeJs(str: string, output: Uint8Array, outputOffset: number): void { 42 | const strLength = str.length; 43 | let offset = outputOffset; 44 | let pos = 0; 45 | while (pos < strLength) { 46 | let value = str.charCodeAt(pos++); 47 | 48 | if ((value & 0xffffff80) === 0) { 49 | // 1-byte 50 | output[offset++] = value; 51 | continue; 52 | } else if ((value & 0xfffff800) === 0) { 53 | // 2-bytes 54 | output[offset++] = ((value >> 6) & 0x1f) | 0xc0; 55 | } else { 56 | // handle surrogate pair 57 | if (value >= 0xd800 && value <= 0xdbff) { 58 | // high surrogate 59 | if (pos < strLength) { 60 | const extra = str.charCodeAt(pos); 61 | if ((extra & 0xfc00) === 0xdc00) { 62 | ++pos; 63 | value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; 64 | } 65 | } 66 | } 67 | 68 | if ((value & 0xffff0000) === 0) { 69 | // 3-byte 70 | output[offset++] = ((value >> 12) & 0x0f) | 0xe0; 71 | output[offset++] = ((value >> 6) & 0x3f) | 0x80; 72 | } else { 73 | // 4-byte 74 | output[offset++] = ((value >> 18) & 0x07) | 0xf0; 75 | output[offset++] = ((value >> 12) & 0x3f) | 0x80; 76 | output[offset++] = ((value >> 6) & 0x3f) | 0x80; 77 | } 78 | } 79 | 80 | output[offset++] = (value & 0x3f) | 0x80; 81 | } 82 | } 83 | 84 | // TextEncoder and TextDecoder are standardized in whatwg encoding: 85 | // https://encoding.spec.whatwg.org/ 86 | // and available in all the modern browsers: 87 | // https://caniuse.com/textencoder 88 | // They are available in Node.js since v12 LTS as well: 89 | // https://nodejs.org/api/globals.html#textencoder 90 | 91 | const sharedTextEncoder = new TextEncoder(); 92 | 93 | // This threshold should be determined by benchmarking, which might vary in engines and input data. 94 | // Run `npx ts-node benchmark/encode-string.ts` for details. 95 | const TEXT_ENCODER_THRESHOLD = 50; 96 | 97 | export function utf8EncodeTE(str: string, output: Uint8Array, outputOffset: number): void { 98 | sharedTextEncoder.encodeInto(str, output.subarray(outputOffset)); 99 | } 100 | 101 | export function utf8Encode(str: string, output: Uint8Array, outputOffset: number): void { 102 | if (str.length > TEXT_ENCODER_THRESHOLD) { 103 | utf8EncodeTE(str, output, outputOffset); 104 | } else { 105 | utf8EncodeJs(str, output, outputOffset); 106 | } 107 | } 108 | 109 | const CHUNK_SIZE = 0x1_000; 110 | 111 | export function utf8DecodeJs(bytes: Uint8Array, inputOffset: number, byteLength: number): string { 112 | let offset = inputOffset; 113 | const end = offset + byteLength; 114 | 115 | const units: Array = []; 116 | let result = ""; 117 | while (offset < end) { 118 | const byte1 = bytes[offset++]!; 119 | if ((byte1 & 0x80) === 0) { 120 | // 1 byte 121 | units.push(byte1); 122 | } else if ((byte1 & 0xe0) === 0xc0) { 123 | // 2 bytes 124 | const byte2 = bytes[offset++]! & 0x3f; 125 | units.push(((byte1 & 0x1f) << 6) | byte2); 126 | } else if ((byte1 & 0xf0) === 0xe0) { 127 | // 3 bytes 128 | const byte2 = bytes[offset++]! & 0x3f; 129 | const byte3 = bytes[offset++]! & 0x3f; 130 | units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); 131 | } else if ((byte1 & 0xf8) === 0xf0) { 132 | // 4 bytes 133 | const byte2 = bytes[offset++]! & 0x3f; 134 | const byte3 = bytes[offset++]! & 0x3f; 135 | const byte4 = bytes[offset++]! & 0x3f; 136 | let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; 137 | if (unit > 0xffff) { 138 | unit -= 0x10000; 139 | units.push(((unit >>> 10) & 0x3ff) | 0xd800); 140 | unit = 0xdc00 | (unit & 0x3ff); 141 | } 142 | units.push(unit); 143 | } else { 144 | units.push(byte1); 145 | } 146 | 147 | if (units.length >= CHUNK_SIZE) { 148 | result += String.fromCharCode(...units); 149 | units.length = 0; 150 | } 151 | } 152 | 153 | if (units.length > 0) { 154 | result += String.fromCharCode(...units); 155 | } 156 | 157 | return result; 158 | } 159 | 160 | const sharedTextDecoder = new TextDecoder(); 161 | 162 | // This threshold should be determined by benchmarking, which might vary in engines and input data. 163 | // Run `npx ts-node benchmark/decode-string.ts` for details. 164 | const TEXT_DECODER_THRESHOLD = 200; 165 | 166 | export function utf8DecodeTD(bytes: Uint8Array, inputOffset: number, byteLength: number): string { 167 | const stringBytes = bytes.subarray(inputOffset, inputOffset + byteLength); 168 | return sharedTextDecoder.decode(stringBytes); 169 | } 170 | 171 | export function utf8Decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string { 172 | if (byteLength > TEXT_DECODER_THRESHOLD) { 173 | return utf8DecodeTD(bytes, inputOffset, byteLength); 174 | } else { 175 | return utf8DecodeJs(bytes, inputOffset, byteLength); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /test/CachedKeyDecoder.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { CachedKeyDecoder } from "../src/CachedKeyDecoder.ts"; 3 | import { utf8EncodeJs, utf8Count } from "../src/utils/utf8.ts"; 4 | import type { KeyDecoder } from "../src/CachedKeyDecoder.ts"; 5 | 6 | function tryDecode(keyDecoder: KeyDecoder, str: string): string { 7 | const byteLength = utf8Count(str); 8 | const bytes = new Uint8Array(byteLength); 9 | utf8EncodeJs(str, bytes, 0); 10 | if (!keyDecoder.canBeCached(byteLength)) { 11 | throw new Error("Unexpected precondition"); 12 | } 13 | return keyDecoder.decode(bytes, 0, byteLength); 14 | } 15 | 16 | describe("CachedKeyDecoder", () => { 17 | context("basic behavior", () => { 18 | it("decodes a string", () => { 19 | const decoder = new CachedKeyDecoder(); 20 | 21 | assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); 22 | assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); 23 | assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); 24 | 25 | // console.dir(decoder, { depth: 100 }); 26 | }); 27 | 28 | it("decodes strings", () => { 29 | const decoder = new CachedKeyDecoder(); 30 | 31 | assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); 32 | assert.deepStrictEqual(tryDecode(decoder, "bar"), "bar"); 33 | assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); 34 | 35 | // console.dir(decoder, { depth: 100 }); 36 | }); 37 | 38 | it("decodes strings with purging records", () => { 39 | const decoder = new CachedKeyDecoder(16, 4); 40 | 41 | for (let i = 0; i < 100; i++) { 42 | assert.deepStrictEqual(tryDecode(decoder, "foo1"), "foo1"); 43 | assert.deepStrictEqual(tryDecode(decoder, "foo2"), "foo2"); 44 | assert.deepStrictEqual(tryDecode(decoder, "foo3"), "foo3"); 45 | assert.deepStrictEqual(tryDecode(decoder, "foo4"), "foo4"); 46 | assert.deepStrictEqual(tryDecode(decoder, "foo5"), "foo5"); 47 | } 48 | 49 | // console.dir(decoder, { depth: 100 }); 50 | }); 51 | }); 52 | 53 | context("edge cases", () => { 54 | // len=0 is not supported because it is just an empty string 55 | it("decodes str with len=1", () => { 56 | const decoder = new CachedKeyDecoder(); 57 | 58 | assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); 59 | assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); 60 | assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); 61 | assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); 62 | 63 | // console.dir(decoder, { depth: 100 }); 64 | }); 65 | 66 | it("decodes str with len=maxKeyLength", () => { 67 | const decoder = new CachedKeyDecoder(1); 68 | 69 | assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); 70 | assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); 71 | assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); 72 | assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); 73 | 74 | //console.dir(decoder, { depth: 100 }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/ExtensionCodec.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import util from "util"; 3 | import { encode, decode, ExtensionCodec, decodeAsync } from "../src/index.ts"; 4 | 5 | describe("ExtensionCodec", () => { 6 | context("timestamp", () => { 7 | const extensionCodec = ExtensionCodec.defaultCodec; 8 | 9 | it("encodes and decodes a date without milliseconds (timestamp 32)", () => { 10 | const date = new Date(1556633024000); 11 | const encoded = encode(date, { extensionCodec }); 12 | assert.deepStrictEqual( 13 | decode(encoded, { extensionCodec }), 14 | date, 15 | `date: ${date.toISOString()}, encoded: ${util.inspect(encoded)}`, 16 | ); 17 | }); 18 | 19 | it("encodes and decodes a date with milliseconds (timestamp 64)", () => { 20 | const date = new Date(1556633024123); 21 | const encoded = encode(date, { extensionCodec }); 22 | assert.deepStrictEqual( 23 | decode(encoded, { extensionCodec }), 24 | date, 25 | `date: ${date.toISOString()}, encoded: ${util.inspect(encoded)}`, 26 | ); 27 | }); 28 | 29 | it("encodes and decodes a future date (timestamp 96)", () => { 30 | const date = new Date(0x400000000 * 1000); 31 | const encoded = encode(date, { extensionCodec }); 32 | assert.deepStrictEqual( 33 | decode(encoded, { extensionCodec }), 34 | date, 35 | `date: ${date.toISOString()}, encoded: ${util.inspect(encoded)}`, 36 | ); 37 | }); 38 | }); 39 | 40 | context("custom extensions", () => { 41 | const extensionCodec = new ExtensionCodec(); 42 | 43 | // Set 44 | extensionCodec.register({ 45 | type: 0, 46 | encode: (object: unknown): Uint8Array | null => { 47 | if (object instanceof Set) { 48 | return encode([...object]); 49 | } else { 50 | return null; 51 | } 52 | }, 53 | decode: (data: Uint8Array) => { 54 | const array = decode(data) as Array; 55 | return new Set(array); 56 | }, 57 | }); 58 | 59 | // Map 60 | extensionCodec.register({ 61 | type: 1, 62 | encode: (object: unknown): Uint8Array | null => { 63 | if (object instanceof Map) { 64 | return encode([...object]); 65 | } else { 66 | return null; 67 | } 68 | }, 69 | decode: (data: Uint8Array) => { 70 | const array = decode(data) as Array<[unknown, unknown]>; 71 | return new Map(array); 72 | }, 73 | }); 74 | 75 | it("encodes and decodes custom data types (synchronously)", () => { 76 | const set = new Set([1, 2, 3]); 77 | const map = new Map([ 78 | ["foo", "bar"], 79 | ["bar", "baz"], 80 | ]); 81 | const encoded = encode([set, map], { extensionCodec }); 82 | assert.deepStrictEqual(decode(encoded, { extensionCodec }), [set, map]); 83 | }); 84 | 85 | it("encodes and decodes custom data types (asynchronously)", async () => { 86 | const set = new Set([1, 2, 3]); 87 | const map = new Map([ 88 | ["foo", "bar"], 89 | ["bar", "baz"], 90 | ]); 91 | const encoded = encode([set, map], { extensionCodec }); 92 | const createStream = async function* () { 93 | yield encoded; 94 | }; 95 | assert.deepStrictEqual(await decodeAsync(createStream(), { extensionCodec }), [set, map]); 96 | }); 97 | }); 98 | 99 | context("custom extensions with custom context", () => { 100 | class Context { 101 | public ctxVal: number; 102 | public expectations: Array = []; 103 | constructor(ctxVal: number) { 104 | this.ctxVal = ctxVal; 105 | } 106 | public hasVisited(val: any) { 107 | this.expectations.push(val); 108 | } 109 | } 110 | const extensionCodec = new ExtensionCodec(); 111 | 112 | class Magic { 113 | public val: T; 114 | constructor(val: T) { 115 | this.val = val; 116 | } 117 | } 118 | 119 | // Magic 120 | extensionCodec.register({ 121 | type: 0, 122 | encode: (object: unknown, context): Uint8Array | null => { 123 | if (object instanceof Magic) { 124 | context.hasVisited({ encoding: object.val }); 125 | return encode({ magic: object.val, ctx: context.ctxVal }, { extensionCodec, context }); 126 | } else { 127 | return null; 128 | } 129 | }, 130 | decode: (data: Uint8Array, extType, context) => { 131 | const decoded = decode(data, { extensionCodec, context }) as { magic: number }; 132 | context.hasVisited({ decoding: decoded.magic, ctx: context.ctxVal }); 133 | return new Magic(decoded.magic); 134 | }, 135 | }); 136 | 137 | it("encodes and decodes custom data types (synchronously)", () => { 138 | const context = new Context(42); 139 | const magic1 = new Magic(17); 140 | const magic2 = new Magic({ foo: new Magic("inner") }); 141 | const test = [magic1, magic2]; 142 | const encoded = encode(test, { extensionCodec, context }); 143 | assert.deepStrictEqual(decode(encoded, { extensionCodec, context }), test); 144 | assert.deepStrictEqual(context.expectations, [ 145 | { 146 | encoding: magic1.val, 147 | }, 148 | { 149 | encoding: magic2.val, 150 | }, 151 | { 152 | encoding: magic2.val.foo.val, 153 | }, 154 | { 155 | ctx: 42, 156 | decoding: magic1.val, 157 | }, 158 | { 159 | ctx: 42, 160 | decoding: magic2.val.foo.val, 161 | }, 162 | { 163 | ctx: 42, 164 | decoding: magic2.val, 165 | }, 166 | ]); 167 | }); 168 | 169 | it("encodes and decodes custom data types (asynchronously)", async () => { 170 | const context = new Context(42); 171 | const magic1 = new Magic(17); 172 | const magic2 = new Magic({ foo: new Magic("inner") }); 173 | const test = [magic1, magic2]; 174 | const encoded = encode(test, { extensionCodec, context }); 175 | const createStream = async function* () { 176 | yield encoded; 177 | }; 178 | assert.deepStrictEqual(await decodeAsync(createStream(), { extensionCodec, context }), test); 179 | assert.deepStrictEqual(context.expectations, [ 180 | { 181 | encoding: magic1.val, 182 | }, 183 | { 184 | encoding: magic2.val, 185 | }, 186 | { 187 | encoding: magic2.val.foo.val, 188 | }, 189 | { 190 | ctx: 42, 191 | decoding: magic1.val, 192 | }, 193 | { 194 | ctx: 42, 195 | decoding: magic2.val.foo.val, 196 | }, 197 | { 198 | ctx: 42, 199 | decoding: magic2.val, 200 | }, 201 | ]); 202 | }); 203 | }); 204 | 205 | context("custom extensions with alignment", () => { 206 | const extensionCodec = new ExtensionCodec(); 207 | 208 | extensionCodec.register({ 209 | type: 0x01, 210 | encode: (object: unknown) => { 211 | if (object instanceof Float32Array) { 212 | return (pos: number) => { 213 | const bpe = Float32Array.BYTES_PER_ELEMENT; 214 | const padding = 1 + ((bpe - ((pos + 1) % bpe)) % bpe); 215 | const data = new Uint8Array(object.buffer); 216 | const result = new Uint8Array(padding + data.length); 217 | result[0] = padding; 218 | result.set(data, padding); 219 | return result; 220 | }; 221 | } 222 | return null; 223 | }, 224 | decode: (data: Uint8Array) => { 225 | const padding = data[0]!; 226 | const bpe = Float32Array.BYTES_PER_ELEMENT; 227 | const offset = data.byteOffset + padding; 228 | const length = data.byteLength - padding; 229 | return new Float32Array(data.buffer, offset, length / bpe); 230 | }, 231 | }); 232 | 233 | it("encodes and decodes Float32Array type with zero-copy", () => { 234 | const data = { 235 | position: new Float32Array([1.1, 2.2, 3.3, 4.4, 5.5]), 236 | }; 237 | const encoded = encode(data, { extensionCodec }); 238 | const decoded = decode(encoded, { extensionCodec }); 239 | assert.deepStrictEqual(decoded, data); 240 | assert.strictEqual(decoded.position.buffer, encoded.buffer); 241 | }); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /test/bigint64.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode } from "../src/index.ts"; 3 | 4 | describe("useBigInt64: true", () => { 5 | before(function () { 6 | if (typeof BigInt === "undefined") { 7 | this.skip(); 8 | } 9 | }); 10 | 11 | it("encodes and decodes 0n", () => { 12 | const value = BigInt(0); 13 | const encoded = encode(value, { useBigInt64: true }); 14 | assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); 15 | }); 16 | 17 | it("encodes and decodes MAX_SAFE_INTEGER+1", () => { 18 | const value = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1); 19 | const encoded = encode(value, { useBigInt64: true }); 20 | assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); 21 | }); 22 | 23 | it("encodes and decodes MIN_SAFE_INTEGER-1", () => { 24 | const value = BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1); 25 | const encoded = encode(value, { useBigInt64: true }); 26 | assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); 27 | }); 28 | 29 | it("encodes and decodes values with numbers and bigints", () => { 30 | const value = { 31 | ints: [0, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], 32 | nums: [Number.NaN, Math.PI, Math.E, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], 33 | bigints: [BigInt(0), BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1), BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1)], 34 | }; 35 | const encoded = encode(value, { useBigInt64: true }); 36 | assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/bun.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "bun:test"; 2 | import { encode, decode } from "../src/index.ts"; 3 | 4 | test("Hello, world!", () => { 5 | const encoded = encode("Hello, world!"); 6 | const decoded = decode(encoded); 7 | expect(decoded).toBe("Hello, world!"); 8 | }); 9 | -------------------------------------------------------------------------------- /test/codec-bigint.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode, ExtensionCodec, DecodeError } from "../src/index.ts"; 3 | 4 | // There's a built-in `useBigInt64: true` option, but a custom codec might be 5 | // better if you'd like to encode bigint to reduce the size of binaries. 6 | 7 | const BIGINT_EXT_TYPE = 0; // Any in 0-127 8 | 9 | const extensionCodec = new ExtensionCodec(); 10 | extensionCodec.register({ 11 | type: BIGINT_EXT_TYPE, 12 | encode(input: unknown): Uint8Array | null { 13 | if (typeof input === "bigint") { 14 | if (input <= Number.MAX_SAFE_INTEGER && input >= Number.MIN_SAFE_INTEGER) { 15 | return encode(Number(input)); 16 | } else { 17 | return encode(String(input)); 18 | } 19 | } else { 20 | return null; 21 | } 22 | }, 23 | decode(data: Uint8Array): bigint { 24 | const val = decode(data); 25 | if (!(typeof val === "string" || typeof val === "number")) { 26 | throw new DecodeError(`unexpected BigInt source: ${val} (${typeof val})`); 27 | } 28 | return BigInt(val); 29 | }, 30 | }); 31 | 32 | describe("codec BigInt", () => { 33 | it("encodes and decodes 0n", () => { 34 | const value = BigInt(0); 35 | const encoded = encode(value, { extensionCodec }); 36 | assert.deepStrictEqual(decode(encoded, { extensionCodec }), value); 37 | }); 38 | 39 | it("encodes and decodes 100n", () => { 40 | const value = BigInt(100); 41 | const encoded = encode(value, { extensionCodec }); 42 | assert.deepStrictEqual(decode(encoded, { extensionCodec }), value); 43 | }); 44 | 45 | it("encodes and decodes -100n", () => { 46 | const value = BigInt(-100); 47 | const encoded = encode(value, { extensionCodec }); 48 | assert.deepStrictEqual(decode(encoded, { extensionCodec }), value); 49 | }); 50 | 51 | it("encodes and decodes MAX_SAFE_INTEGER+1", () => { 52 | const value = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1); 53 | const encoded = encode(value, { extensionCodec }); 54 | assert.deepStrictEqual(decode(encoded, { extensionCodec }), value); 55 | }); 56 | 57 | it("encodes and decodes MIN_SAFE_INTEGER-1", () => { 58 | const value = BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1); 59 | const encoded = encode(value, { extensionCodec }); 60 | assert.deepStrictEqual(decode(encoded, { extensionCodec }), value); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/codec-float.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import * as ieee754 from "ieee754"; 3 | import { decode } from "../src/index.ts"; 4 | 5 | const FLOAT32_TYPE = 0xca; 6 | const FLOAT64_TYPE = 0xcb; 7 | 8 | const SPECS = { 9 | POSITIVE_ZERO: +0.0, 10 | NEGATIVE_ZERO: -0.0, 11 | POSITIVE_INFINITY: Number.POSITIVE_INFINITY, 12 | NEGATIVE_INFINITY: Number.NEGATIVE_INFINITY, 13 | 14 | POSITIVE_VALUE_1: +0.1, 15 | POSITIVE_VALUE_2: +42, 16 | POSITIVE_VALUE_3: +Math.PI, 17 | POSITIVE_VALUE_4: +Math.E, 18 | NEGATIVE_VALUE_1: -0.1, 19 | NEGATIVE_VALUE_2: -42, 20 | NEGATIVE_VALUE_3: -Math.PI, 21 | NEGATIVE_VALUE_4: -Math.E, 22 | 23 | MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER, 24 | MIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER, 25 | 26 | MAX_VALUE: Number.MAX_VALUE, 27 | MIN_VALUE: Number.MIN_VALUE, 28 | } as Record; 29 | 30 | describe("codec: float 32/64", () => { 31 | context("float 32", () => { 32 | for (const [name, value] of Object.entries(SPECS)) { 33 | it(`decodes ${name} (${value})`, () => { 34 | const buf = new Uint8Array(4); 35 | ieee754.write(buf, value, 0, false, 23, 4); 36 | const expected = ieee754.read(buf, 0, false, 23, 4); 37 | 38 | assert.deepStrictEqual(decode([FLOAT32_TYPE, ...buf]), expected, "matched sign"); 39 | assert.notDeepStrictEqual(decode([FLOAT32_TYPE, ...buf]), -expected, "unmatched sign"); 40 | }); 41 | } 42 | 43 | it(`decodes NaN`, () => { 44 | const buf = new Uint8Array(4); 45 | ieee754.write(buf, NaN, 0, false, 23, 4); 46 | const expected = ieee754.read(buf, 0, false, 23, 4); 47 | 48 | assert.deepStrictEqual(decode([FLOAT32_TYPE, ...buf]), expected, "matched sign"); 49 | }); 50 | }); 51 | 52 | context("float 64", () => { 53 | for (const [name, value] of Object.entries(SPECS)) { 54 | it(`decodes ${name} (${value})`, () => { 55 | const buf = new Uint8Array(8); 56 | ieee754.write(buf, value, 0, false, 52, 8); 57 | const expected = ieee754.read(buf, 0, false, 52, 8); 58 | 59 | assert.deepStrictEqual(decode([FLOAT64_TYPE, ...buf]), expected, "matched sign"); 60 | assert.notDeepStrictEqual(decode([FLOAT64_TYPE, ...buf]), -expected, "unmatched sign"); 61 | }); 62 | } 63 | 64 | it(`decodes NaN`, () => { 65 | const buf = new Uint8Array(8); 66 | ieee754.write(buf, NaN, 0, false, 52, 8); 67 | const expected = ieee754.read(buf, 0, false, 52, 8); 68 | 69 | assert.deepStrictEqual(decode([FLOAT64_TYPE, ...buf]), expected, "matched sign"); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/codec-int.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { setInt64, getInt64, getUint64, setUint64 } from "../src/utils/int.ts"; 3 | 4 | const INT64SPECS = { 5 | ZERO: 0, 6 | ONE: 1, 7 | MINUS_ONE: -1, 8 | X_FF: 0xff, 9 | MINUS_X_FF: -0xff, 10 | INT32_MAX: 0x7fffffff, 11 | INT32_MIN: -0x7fffffff - 1, 12 | MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER, 13 | MIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER, 14 | } as Record; 15 | 16 | describe("codec: int64 / uint64", () => { 17 | context("int 64", () => { 18 | for (const name of Object.keys(INT64SPECS)) { 19 | const value = INT64SPECS[name]!; 20 | 21 | it(`sets and gets ${value} (${value < 0 ? "-" : ""}0x${Math.abs(value).toString(16)})`, () => { 22 | const b = new Uint8Array(8); 23 | const view = new DataView(b.buffer); 24 | setInt64(view, 0, value); 25 | assert.deepStrictEqual(getInt64(view, 0), value); 26 | }); 27 | } 28 | }); 29 | 30 | context("uint 64", () => { 31 | it(`sets and gets 0`, () => { 32 | const b = new Uint8Array(8); 33 | const view = new DataView(b.buffer); 34 | setUint64(view, 0, 0); 35 | assert.deepStrictEqual(getUint64(view, 0), 0); 36 | }); 37 | 38 | it(`sets and gets MAX_SAFE_INTEGER`, () => { 39 | const b = new Uint8Array(8); 40 | const view = new DataView(b.buffer); 41 | setUint64(view, 0, Number.MAX_SAFE_INTEGER); 42 | assert.deepStrictEqual(getUint64(view, 0), Number.MAX_SAFE_INTEGER); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/codec-timestamp.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import util from "util"; 3 | import { 4 | encode, 5 | decode, 6 | encodeDateToTimeSpec, 7 | decodeTimestampExtension, 8 | decodeTimestampToTimeSpec, 9 | encodeTimestampExtension, 10 | } from "../src/index.ts"; 11 | 12 | const TIME = 1556636810389; 13 | 14 | const SPECS = { 15 | ZERO: new Date(0), 16 | TIME_BEFORE_EPOCH_NS: new Date(-1), 17 | TIME_BEFORE_EPOCH_SEC: new Date(-1000), 18 | TIME_BEFORE_EPOCH_SEC_AND_NS: new Date(-1002), 19 | TIMESTAMP32: new Date(Math.floor(TIME / 1000) * 1000), 20 | TIMESTAMP64: new Date(TIME), 21 | TIMESTAMP64_OVER_INT32: new Date(Date.UTC(2200, 0)), // cf. https://github.com/msgpack/msgpack-ruby/pull/172 22 | TIMESTAMP96_SEC_OVER_UINT32: new Date(0x400000000 * 1000), 23 | TIMESTAMP96_SEC_OVER_UINT32_WITH_NS: new Date(0x400000000 * 1000 + 2), 24 | 25 | REGRESSION_1: new Date(1556799054803), 26 | } as Record; 27 | 28 | describe("codec: timestamp 32/64/96", () => { 29 | context("encode / decode", () => { 30 | for (const name of Object.keys(SPECS)) { 31 | const value = SPECS[name]!; 32 | 33 | it(`encodes and decodes ${name} (${value.toISOString()})`, () => { 34 | const encoded = encode(value); 35 | assert.deepStrictEqual(decode(encoded), value, `encoded: ${util.inspect(Buffer.from(encoded))}`); 36 | }); 37 | } 38 | }); 39 | 40 | context("encodeDateToTimeSpec", () => { 41 | it("normalizes new Date(-1) to { sec: -1, nsec: 999000000 }", () => { 42 | assert.deepStrictEqual(encodeDateToTimeSpec(new Date(-1)), { sec: -1, nsec: 999000000 }); 43 | }); 44 | }); 45 | 46 | context("encodeDateToTimeSpec", () => { 47 | it("decodes timestamp-ext binary to TimeSpec", () => { 48 | const encoded = encodeTimestampExtension(new Date(42000))!; 49 | assert.deepStrictEqual(decodeTimestampToTimeSpec(encoded), { sec: 42, nsec: 0 }); 50 | }); 51 | }); 52 | 53 | context("decodeTimestampExtension", () => { 54 | context("for broken data", () => { 55 | it("throws errors", () => { 56 | assert.throws(() => { 57 | decodeTimestampExtension(Uint8Array.from([0])); 58 | }, /unrecognized data size for timestamp/i); 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/decode-blob.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode, decodeAsync } from "../src/index.ts"; 3 | 4 | (typeof Blob !== "undefined" ? describe : describe.skip)("Blob", () => { 5 | it("decodes it with `decode()`", async function () { 6 | const blob = new Blob([encode("Hello!")]); 7 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 8 | if (!blob.arrayBuffer) { 9 | this.skip(); 10 | } 11 | assert.deepStrictEqual(decode(await blob.arrayBuffer()), "Hello!"); 12 | }); 13 | 14 | it("decodes it with `decodeAsync()`", async function () { 15 | const blob = new Blob([encode("Hello!")]); 16 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 17 | if (!blob.stream) { 18 | this.skip(); 19 | } 20 | 21 | // use any because the type of Blob#stream() in @types/node does not make sense here. 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 23 | assert.deepStrictEqual(await decodeAsync(blob.stream() as any), "Hello!"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/decode-max-length.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode, decodeAsync } from "../src/index.ts"; 3 | import type { DecoderOptions } from "../src/index.ts"; 4 | 5 | describe("decode with max${Type}Length specified", () => { 6 | async function* createStream(input: T) { 7 | yield input; 8 | } 9 | 10 | context("maxStrLength", () => { 11 | const input = encode("foo"); 12 | const options = { maxStrLength: 1 } satisfies DecoderOptions; 13 | 14 | it("throws errors (synchronous)", () => { 15 | assert.throws(() => { 16 | decode(input, options); 17 | }, /max length exceeded/i); 18 | }); 19 | 20 | it("throws errors (asynchronous)", async () => { 21 | await assert.rejects(async () => { 22 | await decodeAsync(createStream(input), options); 23 | }, /max length exceeded/i); 24 | }); 25 | }); 26 | 27 | context("maxBinLength", () => { 28 | const input = encode(Uint8Array.from([1, 2, 3])); 29 | const options = { maxBinLength: 1 } satisfies DecoderOptions; 30 | 31 | it("throws errors (synchronous)", () => { 32 | assert.throws(() => { 33 | decode(input, options); 34 | }, /max length exceeded/i); 35 | }); 36 | 37 | it("throws errors (asynchronous)", async () => { 38 | await assert.rejects(async () => { 39 | await decodeAsync(createStream(input), options); 40 | }, /max length exceeded/i); 41 | }); 42 | }); 43 | 44 | context("maxArrayLength", () => { 45 | const input = encode([1, 2, 3]); 46 | const options = { maxArrayLength: 1 } satisfies DecoderOptions; 47 | 48 | it("throws errors (synchronous)", () => { 49 | assert.throws(() => { 50 | decode(input, options); 51 | }, /max length exceeded/i); 52 | }); 53 | 54 | it("throws errors (asynchronous)", async () => { 55 | await assert.rejects(async () => { 56 | await decodeAsync(createStream(input), options); 57 | }, /max length exceeded/i); 58 | }); 59 | }); 60 | 61 | context("maxMapLength", () => { 62 | const input = encode({ foo: 1, bar: 1, baz: 3 }); 63 | const options = { maxMapLength: 1 } satisfies DecoderOptions; 64 | 65 | it("throws errors (synchronous)", () => { 66 | assert.throws(() => { 67 | decode(input, options); 68 | }, /max length exceeded/i); 69 | }); 70 | 71 | it("throws errors (asynchronous)", async () => { 72 | await assert.rejects(async () => { 73 | await decodeAsync(createStream(input), options); 74 | }, /max length exceeded/i); 75 | }); 76 | }); 77 | 78 | context("maxExtType", () => { 79 | const input = encode(new Date()); 80 | // timextamp ext requires at least 4 bytes. 81 | const options = { maxExtLength: 1 } satisfies DecoderOptions; 82 | 83 | it("throws errors (synchronous)", () => { 84 | assert.throws(() => { 85 | decode(input, options); 86 | }, /max length exceeded/i); 87 | }); 88 | 89 | it("throws errors (asynchronous)", async () => { 90 | await assert.rejects(async () => { 91 | await decodeAsync(createStream(input), options); 92 | }, /max length exceeded/i); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/decode-raw-strings.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode } from "../src/index.ts"; 3 | import type { DecoderOptions } from "../src/index.ts"; 4 | 5 | describe("decode with rawStrings specified", () => { 6 | const options = { rawStrings: true } satisfies DecoderOptions; 7 | 8 | it("decodes string as binary", () => { 9 | const actual = decode(encode("foo"), options); 10 | const expected = Uint8Array.from([0x66, 0x6f, 0x6f]); 11 | assert.deepStrictEqual(actual, expected); 12 | }); 13 | 14 | it("decodes invalid UTF-8 string as binary", () => { 15 | const invalidUtf8String = Uint8Array.from([ 16 | 61, 180, 118, 220, 39, 166, 43, 68, 219, 116, 105, 84, 121, 46, 122, 136, 233, 221, 15, 174, 247, 19, 50, 176, 17 | 184, 221, 66, 188, 171, 36, 135, 121, 18 | ]); 19 | const encoded = Uint8Array.from([ 20 | 196, 32, 61, 180, 118, 220, 39, 166, 43, 68, 219, 116, 105, 84, 121, 46, 122, 136, 233, 221, 15, 174, 247, 19, 50, 21 | 176, 184, 221, 66, 188, 171, 36, 135, 121, 22 | ]); 23 | 24 | const actual = decode(encoded, options); 25 | assert.deepStrictEqual(actual, invalidUtf8String); 26 | }); 27 | 28 | it("decodes object keys as strings", () => { 29 | const actual = decode(encode({ key: "foo" }), options); 30 | const expected = { key: Uint8Array.from([0x66, 0x6f, 0x6f]) }; 31 | assert.deepStrictEqual(actual, expected); 32 | }); 33 | 34 | it("ignores maxStrLength", () => { 35 | const lengthLimitedOptions = { ...options, maxStrLength: 1 } satisfies DecoderOptions; 36 | 37 | const actual = decode(encode("foo"), lengthLimitedOptions); 38 | const expected = Uint8Array.from([0x66, 0x6f, 0x6f]); 39 | assert.deepStrictEqual(actual, expected); 40 | }); 41 | 42 | it("respects maxBinLength", () => { 43 | const lengthLimitedOptions = { ...options, maxBinLength: 1 } satisfies DecoderOptions; 44 | 45 | assert.throws(() => { 46 | decode(encode("foo"), lengthLimitedOptions); 47 | }, /max length exceeded/i); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/decode.jsfuzz.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const assert = require("node:assert"); 3 | const { Decoder, encode, DecodeError } = require("../dist/index.js"); 4 | 5 | /** 6 | * @param {Buffer} bytes 7 | * @returns {void} 8 | */ 9 | module.exports.fuzz = function fuzz(bytes) { 10 | const decoder = new Decoder(); 11 | try { 12 | decoder.decode(bytes); 13 | } catch (e) { 14 | if (e instanceof DecodeError) { 15 | // ok 16 | } else if (e instanceof RangeError) { 17 | // ok 18 | } else { 19 | throw e; 20 | } 21 | } 22 | 23 | // make sure the decoder instance is not broken 24 | const object = { 25 | foo: 1, 26 | bar: 2, 27 | baz: ["one", "two", "three"], 28 | }; 29 | assert.deepStrictEqual(decoder.decode(encode(object)), object); 30 | } 31 | -------------------------------------------------------------------------------- /test/decodeArrayStream.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decodeArrayStream } from "../src/index.ts"; 3 | 4 | describe("decodeArrayStream", () => { 5 | const generateSampleObject = () => { 6 | return { 7 | id: Math.random(), 8 | name: "test", 9 | }; 10 | }; 11 | 12 | const createStream = async function* (object: any) { 13 | for (const byte of encode(object)) { 14 | yield [byte]; 15 | } 16 | }; 17 | 18 | it("decodes numbers array (array8)", async () => { 19 | const object = [1, 2, 3, 4, 5]; 20 | 21 | const result: Array = []; 22 | 23 | for await (const item of decodeArrayStream(createStream(object))) { 24 | result.push(item); 25 | } 26 | 27 | assert.deepStrictEqual(object, result); 28 | }); 29 | 30 | it("decodes numbers of array (array16)", async () => { 31 | const createStream = async function* () { 32 | yield [0xdc, 0, 3]; 33 | yield encode(1); 34 | yield encode(2); 35 | yield encode(3); 36 | }; 37 | 38 | const result: Array = []; 39 | 40 | for await (const item of decodeArrayStream(createStream())) { 41 | result.push(item); 42 | } 43 | 44 | assert.deepStrictEqual(result, [1, 2, 3]); 45 | }); 46 | 47 | it("decodes numbers of array (array32)", async () => { 48 | const createStream = async function* () { 49 | yield [0xdd, 0, 0, 0, 3]; 50 | yield encode(1); 51 | yield encode(2); 52 | yield encode(3); 53 | }; 54 | 55 | const result: Array = []; 56 | 57 | for await (const item of decodeArrayStream(createStream())) { 58 | result.push(item); 59 | } 60 | 61 | assert.deepStrictEqual(result, [1, 2, 3]); 62 | }); 63 | 64 | it("decodes objects array", async () => { 65 | const objectsArrays: Array = []; 66 | 67 | for (let i = 0; i < 10; i++) { 68 | objectsArrays.push(generateSampleObject()); 69 | } 70 | 71 | const result: Array = []; 72 | 73 | for await (const item of decodeArrayStream(createStream(objectsArrays))) { 74 | result.push(item); 75 | } 76 | 77 | assert.deepStrictEqual(objectsArrays, result); 78 | }); 79 | 80 | it("fails for non array input", async () => { 81 | const object = "demo"; 82 | 83 | await assert.rejects(async () => { 84 | const result: Array = []; 85 | 86 | for await (const item of decodeArrayStream(createStream(object))) { 87 | result.push(item); 88 | } 89 | }, /.*Unrecognized array type byte:.*/i); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/decodeAsync.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decodeAsync } from "../src/index.ts"; 3 | 4 | describe("decodeAsync", () => { 5 | function wrapWithNoisyBuffer(byte: number) { 6 | return Uint8Array.from([0x01, byte, 0x02]).subarray(1, 2); 7 | } 8 | 9 | it("decodes nil", async () => { 10 | const createStream = async function* () { 11 | yield wrapWithNoisyBuffer(0xc0); // nil 12 | }; 13 | 14 | const object = await decodeAsync(createStream()); 15 | assert.deepStrictEqual(object, null); 16 | }); 17 | 18 | it("decodes fixarray [nil]", async () => { 19 | const createStream = async function* () { 20 | yield wrapWithNoisyBuffer(0x91); // fixarray size=1 21 | yield [0xc0]; // nil 22 | }; 23 | 24 | const object = await decodeAsync(createStream()); 25 | assert.deepStrictEqual(object, [null]); 26 | }); 27 | 28 | it("decodes fixmap {'foo': 'bar'}", async () => { 29 | const createStream = async function* () { 30 | yield [0x81]; // fixmap size=1 31 | yield encode("foo"); 32 | yield encode("bar"); 33 | }; 34 | 35 | const object = await decodeAsync(createStream()); 36 | assert.deepStrictEqual(object, { "foo": "bar" }); 37 | }); 38 | 39 | it("decodes fixmap {'[1, 2]': 'baz'} with custom map key converter", async () => { 40 | const createStream = async function* () { 41 | yield [0x81]; // fixmap size=1 42 | yield encode([1, 2]); 43 | yield encode("baz"); 44 | }; 45 | 46 | const object = await decodeAsync(createStream(), { 47 | mapKeyConverter: (key) => JSON.stringify(key), 48 | }); 49 | 50 | const key = JSON.stringify([1, 2]); 51 | assert.deepStrictEqual(object, { [key]: "baz" }); 52 | }); 53 | 54 | it("decodes multi-byte integer byte-by-byte", async () => { 55 | const createStream = async function* () { 56 | yield [0xcd]; // uint 16 57 | yield [0x12]; 58 | yield [0x34]; 59 | }; 60 | const object = await decodeAsync(createStream()); 61 | assert.deepStrictEqual(object, 0x1234); 62 | }); 63 | 64 | it("decodes fixstr byte-by-byte", async () => { 65 | const createStream = async function* () { 66 | yield [0xa3]; // fixstr size=3 67 | yield [0x66]; // "f" 68 | yield [0x6f]; // "o" 69 | yield [0x6f]; // "o" 70 | }; 71 | const object = await decodeAsync(createStream()); 72 | assert.deepStrictEqual(object, "foo"); 73 | }); 74 | 75 | it("decodes binary byte-by-byte", async () => { 76 | const createStream = async function* () { 77 | yield [0xc4]; // bin 8 78 | yield [0x03]; // bin size=3 79 | yield [0x66]; // "f" 80 | yield [0x6f]; // "o" 81 | yield [0x6f]; // "o" 82 | }; 83 | const object = await decodeAsync(createStream()); 84 | assert.deepStrictEqual(object, Uint8Array.from([0x66, 0x6f, 0x6f])); 85 | }); 86 | 87 | it("decodes binary with noisy buffer", async () => { 88 | const createStream = async function* () { 89 | yield wrapWithNoisyBuffer(0xc5); // bin 16 90 | yield [0x00]; 91 | yield [0x00]; // bin size=0 92 | }; 93 | const object = await decodeAsync(createStream()); 94 | assert.deepStrictEqual(object, new Uint8Array(0)); 95 | }); 96 | 97 | it("decodes mixed object byte-by-byte", async () => { 98 | const object = { 99 | nil: null, 100 | true: true, 101 | false: false, 102 | int: -42, 103 | uint64: Number.MAX_SAFE_INTEGER, 104 | int64: Number.MIN_SAFE_INTEGER, 105 | float: Math.PI, 106 | string: "Hello, world!", 107 | longString: "Hello, world!\n".repeat(100), 108 | binary: Uint8Array.from([0xf1, 0xf2, 0xf3]), 109 | array: [1000, 2000, 3000], 110 | map: { foo: 1, bar: 2, baz: 3 }, 111 | timestampExt: new Date(), 112 | map0: {}, 113 | array0: [], 114 | str0: "", 115 | bin0: Uint8Array.from([]), 116 | }; 117 | 118 | const createStream = async function* () { 119 | for (const byte of encode(object)) { 120 | yield [byte]; 121 | } 122 | }; 123 | assert.deepStrictEqual(await decodeAsync(createStream()), object); 124 | }); 125 | 126 | it("decodes BufferSource", async () => { 127 | // https://developer.mozilla.org/en-US/docs/Web/API/BufferSource 128 | const createStream = async function* () { 129 | yield [0x81] as ArrayLike; // fixmap size=1 130 | yield encode("foo") as BufferSource; 131 | yield encode("bar") as BufferSource; 132 | }; 133 | 134 | // createStream() returns AsyncGenerator | BufferSource, ...> 135 | const object = await decodeAsync(createStream()); 136 | assert.deepStrictEqual(object, { "foo": "bar" }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/decodeMulti.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decodeMulti } from "../src/index.ts"; 3 | 4 | describe("decodeMulti", () => { 5 | it("decodes multiple objects in a single binary", () => { 6 | const items = [ 7 | "foo", 8 | 10, 9 | { 10 | name: "bar", 11 | }, 12 | [1, 2, 3], 13 | ]; 14 | 15 | const encodedItems = items.map((item) => encode(item)); 16 | const encoded = new Uint8Array(encodedItems.reduce((p, c) => p + c.byteLength, 0)); 17 | let offset = 0; 18 | for (const encodedItem of encodedItems) { 19 | encoded.set(encodedItem, offset); 20 | offset += encodedItem.byteLength; 21 | } 22 | 23 | const result: Array = []; 24 | 25 | for (const item of decodeMulti(encoded)) { 26 | result.push(item); 27 | } 28 | 29 | assert.deepStrictEqual(result, items); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/decodeMultiStream.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decodeMultiStream } from "../src/index.ts"; 3 | 4 | describe("decodeStream", () => { 5 | it("decodes stream", async () => { 6 | const items = [ 7 | "foo", 8 | 10, 9 | { 10 | name: "bar", 11 | }, 12 | [1, 2, 3], 13 | ]; 14 | 15 | const createStream = async function* (): AsyncGenerator { 16 | for (const item of items) { 17 | yield encode(item); 18 | } 19 | }; 20 | 21 | const result: Array = []; 22 | 23 | for await (const item of decodeMultiStream(createStream())) { 24 | result.push(item); 25 | } 26 | 27 | assert.deepStrictEqual(result, items); 28 | }); 29 | 30 | it("decodes multiple objects in a single binary stream", async () => { 31 | const items = [ 32 | "foo", 33 | 10, 34 | { 35 | name: "bar", 36 | }, 37 | [1, 2, 3], 38 | ]; 39 | 40 | const encodedItems = items.map((item) => encode(item)); 41 | const encoded = new Uint8Array(encodedItems.reduce((p, c) => p + c.byteLength, 0)); 42 | let offset = 0; 43 | for (const encodedItem of encodedItems) { 44 | encoded.set(encodedItem, offset); 45 | offset += encodedItem.byteLength; 46 | } 47 | 48 | const createStream = async function* (): AsyncGenerator { 49 | yield encoded; 50 | }; 51 | 52 | const result: Array = []; 53 | 54 | for await (const item of decodeMultiStream(createStream())) { 55 | result.push(item); 56 | } 57 | 58 | assert.deepStrictEqual(result, items); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/deno_cjs_test.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno test --allow-read 2 | 3 | /* eslint-disable */ 4 | import { deepStrictEqual } from "node:assert"; 5 | import { test } from "node:test"; 6 | import * as msgpack from "../dist.cjs/index.cjs"; 7 | 8 | test("Hello, world!", () => { 9 | const encoded = msgpack.encode("Hello, world!"); 10 | const decoded = msgpack.decode(encoded); 11 | deepStrictEqual(decoded, "Hello, world!"); 12 | }); 13 | -------------------------------------------------------------------------------- /test/deno_test.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno test 2 | 3 | /* eslint-disable */ 4 | import { deepStrictEqual } from "node:assert"; 5 | import { test } from "node:test"; 6 | import * as msgpack from "../mod.ts"; 7 | 8 | test("Hello, world!", () => { 9 | const encoded = msgpack.encode("Hello, world!"); 10 | const decoded = msgpack.decode(encoded); 11 | deepStrictEqual(decoded, "Hello, world!"); 12 | }); 13 | -------------------------------------------------------------------------------- /test/edge-cases.test.ts: -------------------------------------------------------------------------------- 1 | // kind of hand-written fuzzing data 2 | // any errors should not break Encoder/Decoder instance states 3 | import assert from "assert"; 4 | import { encode, decodeAsync, decode, Encoder, Decoder, decodeMulti, decodeMultiStream } from "../src/index.ts"; 5 | 6 | function testEncoder(encoder: Encoder): void { 7 | const object = { 8 | foo: 1, 9 | bar: 2, 10 | baz: ["one", "two", "three"], 11 | }; 12 | assert.deepStrictEqual(decode(encoder.encode(object)), object); 13 | } 14 | 15 | function testDecoder(decoder: Decoder): void { 16 | const object = { 17 | foo: 1, 18 | bar: 2, 19 | baz: ["one", "two", "three"], 20 | }; 21 | assert.deepStrictEqual(decoder.decode(encode(object)), object); 22 | } 23 | 24 | describe("edge cases", () => { 25 | context("try to encode cyclic refs", () => { 26 | it("throws errors on arrays", () => { 27 | const encoder = new Encoder(); 28 | const cyclicRefs: Array = []; 29 | cyclicRefs.push(cyclicRefs); 30 | assert.throws(() => { 31 | encoder.encode(cyclicRefs); 32 | }, /too deep/i); 33 | testEncoder(encoder); 34 | }); 35 | 36 | it("throws errors on objects", () => { 37 | const encoder = new Encoder(); 38 | const cyclicRefs: Record = {}; 39 | cyclicRefs["foo"] = cyclicRefs; 40 | assert.throws(() => { 41 | encoder.encode(cyclicRefs); 42 | }, /too deep/i); 43 | testEncoder(encoder); 44 | }); 45 | }); 46 | 47 | context("try to encode unrecognized objects", () => { 48 | it("throws errors", () => { 49 | const encoder = new Encoder(); 50 | assert.throws(() => { 51 | encode(() => {}); 52 | }, /unrecognized object/i); 53 | testEncoder(encoder); 54 | }); 55 | }); 56 | 57 | context("try to decode a map with non-string keys (asynchronous)", () => { 58 | it("throws errors", async () => { 59 | const decoder = new Decoder(); 60 | const createStream = async function* () { 61 | yield [0x81]; // fixmap size=1 62 | yield encode(null); 63 | yield encode(null); 64 | }; 65 | 66 | await assert.rejects(async () => { 67 | await decoder.decodeAsync(createStream()); 68 | }, /The type of key must be string/i); 69 | testDecoder(decoder); 70 | }); 71 | }); 72 | 73 | context("try to decode invalid MessagePack binary", () => { 74 | it("throws errors", () => { 75 | const decoder = new Decoder(); 76 | const TYPE_NEVER_USED = 0xc1; 77 | 78 | assert.throws(() => { 79 | decoder.decode([TYPE_NEVER_USED]); 80 | }, /unrecognized type byte/i); 81 | testDecoder(decoder); 82 | }); 83 | }); 84 | 85 | context("try to decode insufficient data", () => { 86 | it("throws errors (synchronous)", () => { 87 | const decoder = new Decoder(); 88 | assert.throws(() => { 89 | decoder.decode([ 90 | 0x92, // fixarray size=2 91 | 0xc0, // nil 92 | ]); 93 | }, RangeError); 94 | testDecoder(decoder); 95 | }); 96 | 97 | it("throws errors (asynchronous)", async () => { 98 | const decoder = new Decoder(); 99 | const createStream = async function* () { 100 | yield [0x92]; // fixarray size=2 101 | yield encode(null); 102 | }; 103 | 104 | await assert.rejects(async () => { 105 | await decoder.decodeAsync(createStream()); 106 | }, RangeError); 107 | testDecoder(decoder); 108 | }); 109 | }); 110 | 111 | context("try to decode data with extra bytes", () => { 112 | it("throws errors (synchronous)", () => { 113 | const decoder = new Decoder(); 114 | assert.throws(() => { 115 | decoder.decode([ 116 | 0x90, // fixarray size=0 117 | ...encode(null), 118 | ]); 119 | }, RangeError); 120 | testDecoder(decoder); 121 | }); 122 | 123 | it("throws errors (asynchronous)", async () => { 124 | const decoder = new Decoder(); 125 | const createStream = async function* () { 126 | yield [0x90]; // fixarray size=0 127 | yield encode(null); 128 | }; 129 | 130 | await assert.rejects(async () => { 131 | await decoder.decodeAsync(createStream()); 132 | }, RangeError); 133 | testDecoder(decoder); 134 | }); 135 | 136 | it("throws errors (asynchronous)", async () => { 137 | const decoder = new Decoder(); 138 | const createStream = async function* () { 139 | yield [0x90, ...encode(null)]; // fixarray size=0 + nil 140 | }; 141 | 142 | await assert.rejects(async () => { 143 | await decoder.decodeAsync(createStream()); 144 | }, RangeError); 145 | testDecoder(decoder); 146 | }); 147 | }); 148 | 149 | context("try to decode an empty input", () => { 150 | it("throws RangeError (synchronous)", () => { 151 | assert.throws(() => { 152 | decode([]); 153 | }, RangeError); 154 | }); 155 | 156 | it("decodes an empty array with decodeMulti()", () => { 157 | assert.deepStrictEqual([...decodeMulti([])], []); 158 | }); 159 | 160 | it("throws RangeError (asynchronous)", async () => { 161 | const createStream = async function* () { 162 | yield []; 163 | }; 164 | 165 | assert.rejects(async () => { 166 | await decodeAsync(createStream()); 167 | }, RangeError); 168 | }); 169 | 170 | it("decodes an empty array with decodeMultiStream()", async () => { 171 | const createStream = async function* () { 172 | yield []; 173 | }; 174 | 175 | const results: Array = []; 176 | for await (const item of decodeMultiStream(createStream())) { 177 | results.push(item); 178 | } 179 | assert.deepStrictEqual(results, []); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /test/encode.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode } from "../src/index.ts"; 3 | 4 | describe("encode", () => { 5 | context("sortKeys", () => { 6 | it("cannonicalizes encoded binaries", () => { 7 | assert.deepStrictEqual(encode({ a: 1, b: 2 }, { sortKeys: true }), encode({ b: 2, a: 1 }, { sortKeys: true })); 8 | }); 9 | }); 10 | 11 | context("forceFloat32", () => { 12 | it("encodes numbers in float64 without forceFloat32", () => { 13 | assert.deepStrictEqual(encode(3.14), Uint8Array.from([0xcb, 0x40, 0x9, 0x1e, 0xb8, 0x51, 0xeb, 0x85, 0x1f])); 14 | }); 15 | 16 | it("encodes numbers in float32 when forceFloat32=true", () => { 17 | assert.deepStrictEqual(encode(3.14, { forceFloat32: true }), Uint8Array.from([0xca, 0x40, 0x48, 0xf5, 0xc3])); 18 | }); 19 | 20 | it("encodes numbers in float64 with forceFloat32=false", () => { 21 | assert.deepStrictEqual( 22 | encode(3.14, { forceFloat32: false }), 23 | Uint8Array.from([0xcb, 0x40, 0x9, 0x1e, 0xb8, 0x51, 0xeb, 0x85, 0x1f]), 24 | ); 25 | }); 26 | }); 27 | 28 | context("forceFloat", () => { 29 | it("encodes integers as integers without forceIntegerToFloat", () => { 30 | assert.deepStrictEqual(encode(3), Uint8Array.from([0x3])); 31 | }); 32 | 33 | it("encodes integers as floating point when forceIntegerToFloat=true", () => { 34 | assert.deepStrictEqual( 35 | encode(3, { forceIntegerToFloat: true }), 36 | Uint8Array.from([0xcb, 0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 37 | ); 38 | }); 39 | 40 | it("encodes integers as float32 when forceIntegerToFloat=true and forceFloat32=true", () => { 41 | assert.deepStrictEqual( 42 | encode(3, { forceIntegerToFloat: true, forceFloat32: true }), 43 | Uint8Array.from([0xca, 0x40, 0x40, 0x00, 0x00]), 44 | ); 45 | }); 46 | 47 | it("encodes integers as integers when forceIntegerToFloat=false", () => { 48 | assert.deepStrictEqual(encode(3, { forceIntegerToFloat: false }), Uint8Array.from([0x3])); 49 | }); 50 | }); 51 | 52 | context("ignoreUndefined", () => { 53 | it("encodes { foo: undefined } as is by default", () => { 54 | assert.deepStrictEqual(decode(encode({ foo: undefined, bar: 42 })), { foo: null, bar: 42 }); 55 | }); 56 | 57 | it("encodes { foo: undefined } as is with `ignoreUndefined: false`", () => { 58 | assert.deepStrictEqual(decode(encode({ foo: undefined, bar: 42 }, { ignoreUndefined: false })), { 59 | foo: null, 60 | bar: 42, 61 | }); 62 | }); 63 | 64 | it("encodes { foo: undefined } to {} with `ignoreUndefined: true`", () => { 65 | assert.deepStrictEqual(decode(encode({ foo: undefined, bar: 42 }, { ignoreUndefined: true })), { bar: 42 }); 66 | }); 67 | }); 68 | 69 | context("ArrayBuffer as buffer", () => { 70 | const buffer = encode([1, 2, 3]); 71 | const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteLength); 72 | assert.deepStrictEqual(decode(arrayBuffer), decode(buffer)); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/karma-run.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // the util module requires process.env 3 | (globalThis as any).process = { 4 | env: {}, 5 | }; 6 | 7 | (globalThis as any).Buffer = require("buffer").Buffer; 8 | 9 | // import "util" first, 10 | // because core-js breaks the util polyfll (https://github.com/browserify/node-util) on IE11. 11 | require("util"); 12 | 13 | require("core-js"); 14 | 15 | const testsContext = (require as any).context(".", true, /\.test\.ts$/); 16 | 17 | testsContext.keys().forEach(testsContext); 18 | -------------------------------------------------------------------------------- /test/msgpack-ext.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { encode, decode, ExtData } from "../src/index.ts"; 3 | 4 | function seq(n: number) { 5 | const a: Array = []; 6 | for (let i = 0; i < n; i++) { 7 | a.push((i + 1) % 0xff); 8 | } 9 | return Uint8Array.from(a); 10 | } 11 | 12 | describe("msgpack-ext", () => { 13 | const SPECS = { 14 | FIXEXT1: [0xd4, new ExtData(0, seq(1))], 15 | FIXEXT2: [0xd5, new ExtData(0, seq(2))], 16 | FIXEXT4: [0xd6, new ExtData(0, seq(4))], 17 | FIXEXT8: [0xd7, new ExtData(0, seq(8))], 18 | FIXEXT16: [0xd8, new ExtData(0, seq(16))], 19 | EXT8: [0xc7, new ExtData(0, seq(17))], 20 | EXT16: [0xc8, new ExtData(0, seq(0x100))], 21 | EXT32: [0xc9, new ExtData(0, seq(0x10000))], 22 | } as Record; 23 | 24 | for (const name of Object.keys(SPECS)) { 25 | const [msgpackType, extData] = SPECS[name]!; 26 | 27 | it(`preserves ExtData by decode(encode(${name}))`, () => { 28 | const encoded = encode(extData); 29 | assert.strictEqual(encoded[0], msgpackType); 30 | assert.deepStrictEqual(decode(encoded), extData); 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /test/msgpack-test-suite.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import util from "util"; 3 | import { Exam } from "msgpack-test-js"; 4 | import { MsgTimestamp } from "msg-timestamp"; 5 | import { encode, decode, ExtensionCodec, EXT_TIMESTAMP, encodeTimeSpecToTimestamp } from "../src/index.ts"; 6 | 7 | const extensionCodec = new ExtensionCodec(); 8 | extensionCodec.register({ 9 | type: EXT_TIMESTAMP, 10 | encode: (input) => { 11 | if (input instanceof MsgTimestamp) { 12 | return encodeTimeSpecToTimestamp({ 13 | sec: input.getTime(), 14 | nsec: input.getNano(), 15 | }); 16 | } else { 17 | return null; 18 | } 19 | }, 20 | decode: (data: Uint8Array) => { 21 | return MsgTimestamp.parse(Buffer.from(data)); 22 | }, 23 | }); 24 | 25 | const TEST_TYPES = { 26 | array: 1, 27 | bignum: 0, // TODO 28 | binary: 1, 29 | bool: 1, 30 | map: 1, 31 | nil: 1, 32 | number: 1, 33 | string: 1, 34 | timestamp: 1, 35 | }; 36 | 37 | describe("msgpack-test-suite", () => { 38 | Exam.getExams(TEST_TYPES).forEach((exam) => { 39 | const types = exam.getTypes(TEST_TYPES); 40 | const first = types[0]!; 41 | const title = `${first}: ${exam.stringify(first)}`; 42 | it(`encodes ${title}`, () => { 43 | types.forEach((type) => { 44 | const value = exam.getValue(type); 45 | const buffer = Buffer.from(encode(value, { extensionCodec })); 46 | 47 | if (exam.matchMsgpack(buffer)) { 48 | assert(true, exam.stringify(type)); 49 | } else { 50 | const msg = `encode(${util.inspect(value)}): expect ${util.inspect(buffer)} to be one of ${util.inspect( 51 | exam.getMsgpacks(), 52 | )}`; 53 | assert(false, msg); 54 | } 55 | }); 56 | }); 57 | 58 | it(`decodes ${title}`, () => { 59 | const msgpacks = exam.getMsgpacks(); 60 | msgpacks.forEach((encoded, idx) => { 61 | const value = decode(encoded, { extensionCodec }); 62 | if (exam.matchValue(value)) { 63 | assert(true, exam.stringify(idx)); 64 | } else { 65 | const values = exam.getTypes().map((type) => exam.getValue(type)); 66 | const msg = `decode(${util.inspect(encoded)}): expect ${util.inspect(value)} to be one of ${util.inspect( 67 | values, 68 | )}`; 69 | assert(false, msg); 70 | } 71 | }); 72 | }); 73 | }); 74 | 75 | context("specs not covered by msgpack-test-js", () => { 76 | // by detecting test coverage 77 | const SPECS = { 78 | FLOAT64_POSITIVE_INF: Number.POSITIVE_INFINITY, 79 | FLOAT64_NEGATIVE_INF: Number.NEGATIVE_INFINITY, 80 | FLOAT64_NAN: Number.NaN, 81 | STR16: "a".repeat(0x100), 82 | STR16_MBS: "🌏".repeat(0x100), 83 | STR32: "b".repeat(0x10_000), 84 | STR32_MBS: "🍣".repeat(0x10_000), 85 | STR32LARGE: "c".repeat(0x50_000), // may cause "RangeError: Maximum call stack size exceeded" in simple implelementions 86 | STR_INCLUDING_NUL: "foo\0bar\0", 87 | STR_BROKEN_FF: "\xff", 88 | BIN16: new Uint8Array(0x100).fill(0xff), 89 | BIN32: new Uint8Array(0x10_000).fill(0xff), 90 | BIN32LARGE: new Uint8Array(0x50_000).fill(0xff), // regression: caused "RangeError: Maximum call stack size exceeded" 91 | ARRAY16: new Array(0x100).fill(true), 92 | ARRAY32: new Array(0x10000).fill(true), 93 | MAP16: new Array(0x100).fill(null).reduce>((acc, _val, i) => { 94 | acc[`k${i}`] = i; 95 | return acc; 96 | }, {}), 97 | MAP32: new Array(0x10000).fill(null).reduce>((acc, _val, i) => { 98 | acc[`k${i}`] = i; 99 | return acc; 100 | }, {}), 101 | MIXED: new Array(0x10).fill(Number.MAX_SAFE_INTEGER), 102 | } as Record; 103 | 104 | for (const name of Object.keys(SPECS)) { 105 | const value = SPECS[name]; 106 | 107 | it(`encodes and decodes ${name}`, () => { 108 | const encoded = encode(value); 109 | assert.deepStrictEqual(decode(new Uint8Array(encoded)), value); 110 | }); 111 | } 112 | }); 113 | 114 | describe("encoding in minimum values", () => { 115 | it("int 8", () => { 116 | assert.deepStrictEqual(encode(-128), Uint8Array.from([0xd0, 0x80])); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/prototype-pollution.test.ts: -------------------------------------------------------------------------------- 1 | import { throws } from "assert"; 2 | import { encode, decode, DecodeError } from "../src/index.ts"; 3 | 4 | describe("prototype pollution", () => { 5 | context("__proto__ exists as a map key", () => { 6 | it("raises DecodeError in decoding", () => { 7 | const o = { 8 | foo: "bar", 9 | }; 10 | // override __proto__ as an enumerable property 11 | Object.defineProperty(o, "__proto__", { 12 | value: new Date(0), 13 | enumerable: true, 14 | }); 15 | const encoded = encode(o); 16 | 17 | throws(() => { 18 | decode(encoded); 19 | }, DecodeError); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/readme.test.ts: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual } from "assert"; 2 | import { encode, decode } from "../src/index"; 3 | 4 | describe("README", () => { 5 | context("## Synopsis", () => { 6 | it("runs", () => { 7 | const object = { 8 | nil: null, 9 | integer: 1, 10 | float: Math.PI, 11 | string: "Hello, world!", 12 | binary: Uint8Array.from([1, 2, 3]), 13 | array: [10, 20, 30], 14 | map: { foo: "bar" }, 15 | timestampExt: new Date(), 16 | }; 17 | 18 | const encoded = encode(object); 19 | // encoded is an Uint8Array instance 20 | 21 | deepStrictEqual(decode(encoded), object); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/reuse-instances-with-extensions.test.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/msgpack/msgpack-javascript/issues/195 2 | 3 | import { deepStrictEqual } from "assert"; 4 | import { Encoder, Decoder, ExtensionCodec } from "../src/index.ts"; 5 | 6 | const MSGPACK_EXT_TYPE_BIGINT = 0; 7 | 8 | function registerCodecs(context: MsgPackContext) { 9 | const { extensionCodec, encode, decode } = context; 10 | 11 | extensionCodec.register({ 12 | type: MSGPACK_EXT_TYPE_BIGINT, 13 | encode: (value) => (typeof value === "bigint" ? encode(value.toString()) : null), 14 | decode: (data) => BigInt(decode(data) as string), 15 | }); 16 | } 17 | 18 | class MsgPackContext { 19 | readonly encode: (value: unknown) => Uint8Array; 20 | readonly decode: (buffer: BufferSource | ArrayLike) => unknown; 21 | readonly extensionCodec = new ExtensionCodec(); 22 | 23 | constructor() { 24 | const encoder = new Encoder({ extensionCodec: this.extensionCodec, context: this }); 25 | const decoder = new Decoder({ extensionCodec: this.extensionCodec, context: this }); 26 | 27 | this.encode = encoder.encode.bind(encoder); 28 | this.decode = decoder.decode.bind(decoder); 29 | 30 | registerCodecs(this); 31 | } 32 | } 33 | 34 | describe("reuse instances with extensions", () => { 35 | it("should encode and decode a bigint", () => { 36 | const context = new MsgPackContext(); 37 | const buf = context.encode(BigInt(42)); 38 | const data = context.decode(buf); 39 | deepStrictEqual(data, BigInt(42)); 40 | }); 41 | 42 | it("should encode and decode bigints", () => { 43 | const context = new MsgPackContext(); 44 | const buf = context.encode([BigInt(1), BigInt(2), BigInt(3)]); 45 | const data = context.decode(buf); 46 | deepStrictEqual(data, [BigInt(1), BigInt(2), BigInt(3)]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/reuse-instances.test.ts: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual } from "assert"; 2 | import { Encoder, Decoder, decode } from "../src/index.ts"; 3 | 4 | const createStream = async function* (...args: any) { 5 | for (const item of args) { 6 | yield item; 7 | } 8 | }; 9 | 10 | const N = 10; 11 | 12 | describe("shared instances", () => { 13 | context("encode() and decodeSync()", () => { 14 | it("runs multiple times", () => { 15 | const encoder = new Encoder(); 16 | const decoder = new Decoder(); 17 | 18 | const object = { 19 | nil: null, 20 | integer: 1, 21 | float: Math.PI, 22 | string: "Hello, world!", 23 | binary: Uint8Array.from([1, 2, 3]), 24 | array: [10, 20, 30], 25 | map: { foo: "bar" }, 26 | timestampExt: new Date(), 27 | }; 28 | 29 | for (let i = 0; i < N; i++) { 30 | const encoded: Uint8Array = encoder.encode(object); 31 | deepStrictEqual(decoder.decode(encoded), object, `#${i}`); 32 | } 33 | }); 34 | }); 35 | 36 | context("encode() and decodeAsync()", () => { 37 | it("runs multiple times", async () => { 38 | const encoder = new Encoder(); 39 | const decoder = new Decoder(); 40 | 41 | const object = { 42 | nil: null, 43 | integer: 1, 44 | float: Math.PI, 45 | string: "Hello, world!", 46 | binary: Uint8Array.from([1, 2, 3]), 47 | array: [10, 20, 30], 48 | map: { foo: "bar" }, 49 | timestampExt: new Date(), 50 | }; 51 | 52 | for (let i = 0; i < N; i++) { 53 | const encoded: Uint8Array = encoder.encode(object); 54 | deepStrictEqual(await decoder.decodeAsync(createStream(encoded)), object, `#${i}`); 55 | } 56 | }); 57 | }); 58 | 59 | context("encode() and decodeStream()", () => { 60 | it("runs multiple times", async () => { 61 | const encoder = new Encoder(); 62 | const decoder = new Decoder(); 63 | 64 | const object = { 65 | nil: null, 66 | integer: 1, 67 | float: Math.PI, 68 | string: "Hello, world!", 69 | binary: Uint8Array.from([1, 2, 3]), 70 | array: [10, 20, 30], 71 | map: { foo: "bar" }, 72 | timestampExt: new Date(), 73 | }; 74 | 75 | for (let i = 0; i < N; i++) { 76 | const encoded: Uint8Array = encoder.encode(object); 77 | const a: Array = []; 78 | for await (const item of decoder.decodeStream(createStream(encoded))) { 79 | a.push(item); 80 | } 81 | deepStrictEqual(a, [object], `#${i}`); 82 | } 83 | }); 84 | }); 85 | 86 | context("encode() and decodeArrayStream()", () => { 87 | it("runs multiple times", async () => { 88 | const encoder = new Encoder(); 89 | const decoder = new Decoder(); 90 | 91 | const object = { 92 | nil: null, 93 | integer: 1, 94 | float: Math.PI, 95 | string: "Hello, world!", 96 | binary: Uint8Array.from([1, 2, 3]), 97 | array: [10, 20, 30], 98 | map: { foo: "bar" }, 99 | timestampExt: new Date(), 100 | }; 101 | 102 | for (let i = 0; i < N; i++) { 103 | const encoded: Uint8Array = encoder.encode([object]); 104 | const a: Array = []; 105 | for await (const item of decoder.decodeStream(createStream(encoded))) { 106 | a.push(item); 107 | } 108 | deepStrictEqual(a, [[object]], `#${i}`); 109 | } 110 | }); 111 | 112 | context("regression #212", () => { 113 | it("runs multiple times", () => { 114 | const encoder = new Encoder(); 115 | const decoder = new Decoder(); 116 | 117 | const data1 = { 118 | isCommunication: false, 119 | isWarning: false, 120 | alarmId: "619f65a2774abf00568b7210", 121 | intervalStart: "2022-05-20T12:00:00.000Z", 122 | intervalStop: "2022-05-20T13:00:00.000Z", 123 | triggeredAt: "2022-05-20T13:00:00.000Z", 124 | component: "someComponent", 125 | _id: "6287920245a582301475627d", 126 | }; 127 | 128 | const data2 = { 129 | foo: "bar", 130 | }; 131 | 132 | const arr = [data1, data2]; 133 | const enc = arr.map((x) => [x, encoder.encode(x)] as const); 134 | 135 | enc.forEach(([orig, acc]) => { 136 | const des = decoder.decode(acc); 137 | deepStrictEqual(des, orig); 138 | }); 139 | }); 140 | }); 141 | 142 | context("Encoder#encodeSharedRef()", () => { 143 | it("returns the shared reference", () => { 144 | const encoder = new Encoder(); 145 | 146 | const a = encoder.encodeSharedRef(true); 147 | const b = encoder.encodeSharedRef(false); 148 | 149 | deepStrictEqual(decode(a), decode(b)); // yes, this is the expected behavior 150 | deepStrictEqual(a.buffer, b.buffer); 151 | }); 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /test/whatwg-streams.test.ts: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual } from "assert"; 2 | import { decodeAsync, encode, decodeArrayStream } from "../src/index.ts"; 3 | 4 | const isReadableStreamConstructorAvailable: boolean = (() => { 5 | try { 6 | // Edge <= 18 has ReadableStream but its constructor is not available 7 | new ReadableStream({ 8 | start(_controller) {}, 9 | }); 10 | return true; 11 | } catch { 12 | return false; 13 | } 14 | })(); 15 | 16 | // Downgrade stream not to implement AsyncIterable 17 | function downgradeReadableStream(stream: ReadableStream) { 18 | (stream as any)[Symbol.asyncIterator] = undefined; 19 | } 20 | 21 | (isReadableStreamConstructorAvailable ? describe : describe.skip)("whatwg streams", () => { 22 | it("decodeArrayStream", async () => { 23 | const data = [1, 2, 3]; 24 | const encoded = encode(data); 25 | const stream = new ReadableStream({ 26 | start(controller) { 27 | for (const byte of encoded) { 28 | controller.enqueue([byte]); 29 | } 30 | controller.close(); 31 | }, 32 | }); 33 | downgradeReadableStream(stream); 34 | 35 | const items: Array = []; 36 | for await (const item of decodeArrayStream(stream)) { 37 | items.push(item); 38 | } 39 | deepStrictEqual(items, data); 40 | }); 41 | 42 | it("decodeAsync", async () => { 43 | const data = [1, 2, 3]; 44 | const encoded = encode(data); 45 | const stream = new ReadableStream({ 46 | start(controller) { 47 | for (const byte of encoded) { 48 | controller.enqueue([byte]); 49 | } 50 | controller.close(); 51 | }, 52 | }); 53 | downgradeReadableStream(stream); 54 | 55 | deepStrictEqual(await decodeAsync(stream), data); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tools/fix-ext.mts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | const mode = process.argv[2]; // --cjs or --mjs 4 | const files = process.argv.slice(3); 5 | 6 | const ext = mode === "--cjs" ? "cjs" : "mjs"; 7 | 8 | console.info(`Fixing ${mode} files with extension ${ext}`); 9 | 10 | for (const file of files) { 11 | const fileMjs = file.replace(/\.js$/, `.${ext}`); 12 | console.info(`Processing ${file} => ${fileMjs}`); 13 | // .js => .mjs 14 | const content = fs.readFileSync(file).toString("utf-8"); 15 | const newContent = content 16 | .replace(/\bfrom "(\.\.?\/[^"]+)\.js";/g, `from "$1.${ext}";`) 17 | .replace(/\bimport "(\.\.?\/[^"]+)\.js";/g, `import "$1.${ext}";`) 18 | .replace(/\brequire\("(\.\.?\/[^"]+)\.js"\)/g, `require("$1.${ext}");`) 19 | .replace(/\/\/# sourceMappingURL=(.+)\.js\.map$/, `//# sourceMappingURL=$1.${ext}.map`); 20 | fs.writeFileSync(fileMjs, newContent); 21 | fs.unlinkSync(file); 22 | 23 | // .js.map => .mjs.map 24 | const mapping = JSON.parse(fs.readFileSync(`${file}.map`).toString("utf-8")); 25 | mapping.file = mapping.file.replace(/\.js$/, ext); 26 | fs.writeFileSync(`${fileMjs}.map`, JSON.stringify(mapping)); 27 | fs.unlinkSync(`${file}.map`); 28 | } 29 | -------------------------------------------------------------------------------- /tools/get-release-tag.mjs: -------------------------------------------------------------------------------- 1 | 2 | import packageJson from "../package.json" with { type: "json" }; 3 | 4 | const matched = /-(beta|rc)\d+$/.exec(packageJson.version); 5 | console.log(matched?.[1] ?? "latest"); 6 | -------------------------------------------------------------------------------- /tsconfig.dist.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./dist.cjs", 6 | "declaration": false, 7 | "noEmitOnError": true, 8 | "noEmit": false, 9 | "rewriteRelativeImportExtensions": true, 10 | "incremental": false 11 | }, 12 | "include": ["src/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.dist.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "./dist.esm", 6 | "declaration": true, 7 | "noEmitOnError": true, 8 | "noEmit": false, 9 | "rewriteRelativeImportExtensions": true, 10 | "incremental": false 11 | }, 12 | "include": ["src/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.dist.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "noEmitOnError": true, 6 | "noEmit": false, 7 | "allowImportingTsExtensions": false, 8 | "outDir": "./build/webpack" 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2020", /* the baseline */ 5 | "module": "CommonJS", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["ES2024", "DOM"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./build", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | "incremental": true, /* Enable incremental compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | "importHelpers": false, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | "noUncheckedIndexedAccess": true, 41 | "noPropertyAccessFromIndexSignature": true, 42 | "noImplicitOverride": true, 43 | "verbatimModuleSyntax": false, 44 | "allowImportingTsExtensions": true, 45 | "noEmit": true, 46 | 47 | /* Module Resolution Options */ 48 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 49 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 50 | // "paths": { 51 | // "@msgpack/msgpack": ["./src"] 52 | // }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 54 | // "typeRoots": [], /* List of folders to include type definitions from. */ 55 | // "types": [], /* Type declaration files to be included in compilation. */ 56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 57 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 59 | "resolveJsonModule": true, 60 | "skipLibCheck": true, 61 | "forceConsistentCasingInFileNames": true 62 | 63 | // "erasableSyntaxOnly": true 64 | 65 | /* Source Map Options */ 66 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 69 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 70 | 71 | /* Experimental Options */ 72 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 73 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 74 | }, 75 | "exclude": ["example", "benchmark", "test/bun*", "test/deno*", "mod.ts"] 76 | } 77 | -------------------------------------------------------------------------------- /tsconfig.test-karma.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "noEmitOnError": true, 6 | "noEmit": false, 7 | "allowImportingTsExtensions": false, 8 | "outDir": "./build/karma", 9 | "incremental": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import url from "node:url"; 3 | import webpack from "webpack"; 4 | import _ from "lodash"; 5 | 6 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 7 | 8 | const config = { 9 | mode: "production", 10 | 11 | entry: "./src/index.ts", 12 | target: ["web", "es2020"], 13 | output: { 14 | path: path.resolve(dirname, "dist.umd"), 15 | library: "MessagePack", 16 | libraryTarget: "umd", 17 | globalObject: "this", 18 | filename: undefined, 19 | }, 20 | resolve: { 21 | extensions: [".ts", ".tsx", ".mjs", ".js", ".json", ".wasm"], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.tsx?$/, 27 | loader: "ts-loader", 28 | options: { 29 | transpileOnly: true, 30 | configFile: "tsconfig.dist.webpack.json", 31 | }, 32 | }, 33 | ], 34 | }, 35 | 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | "process.env.TEXT_ENCODING": "undefined", 39 | "process.env.TEXT_DECODER": "undefined", 40 | }), 41 | ], 42 | 43 | optimization: { 44 | minimize: undefined, 45 | }, 46 | 47 | // We don't need NodeJS stuff on browsers! 48 | // https://webpack.js.org/configuration/node/ 49 | node: false, 50 | 51 | devtool: "source-map", 52 | }; 53 | 54 | 55 | export default [ 56 | ((config) => { 57 | config.output.filename = "msgpack.min.js"; 58 | config.optimization.minimize = true; 59 | return config; 60 | })(_.cloneDeep(config)), 61 | 62 | ((config) => { 63 | config.output.filename = "msgpack.js"; 64 | config.optimization.minimize = false; 65 | return config; 66 | })(_.cloneDeep(config)), 67 | ]; 68 | --------------------------------------------------------------------------------