├── docs └── .gitkeep ├── _config.yml ├── src ├── index.ts ├── toUtf16.ts ├── toUtf16.test.ts ├── toUtf8.perf.ts ├── toUtf8.test.ts ├── xxHash32.test.ts ├── toUtf8.ts └── xxHash32.ts ├── index.mjs ├── cspellrc.json ├── .prettierrc.json ├── .github ├── workflows │ ├── cspell-cli.yml │ ├── lint.yml │ ├── publish.yaml │ ├── codeql-analysis.yml │ ├── release-please.yml │ └── test.yml ├── FUNDING.yml └── dependabot.yml ├── test ├── package.json ├── tsconfig.json ├── test.mjs ├── test.cjs └── perf │ └── xxhash.perf.mts ├── .prettierignore ├── cspell.json ├── .editorconfig ├── tsconfig.esm.json ├── tsconfig.cjs.json ├── tsconfig.json ├── eslint.config.ts ├── .vscode └── launch.json ├── LICENSE ├── .gitignore ├── CHANGELOG.md ├── package.json └── README.md /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { xxHash32 } from './xxHash32.js'; 2 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | export { xxHash32 } from './dist/esm/index.js'; 2 | -------------------------------------------------------------------------------- /cspellrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["**"], 3 | "import": ["./cspell.json"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "overrides": [ 5 | { 6 | "files": "**/*.{yaml,yml}", 7 | "options": { 8 | "singleQuote": false 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/cspell-cli.yml: -------------------------------------------------------------------------------- 1 | name: cSpell-cli 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | cspell: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - run: npx cspell@latest -c cspellrc.json 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: actions/setup-node@v6 15 | - run: npm ci 16 | - run: npm run lint 17 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-xxhash", 3 | "type": "module", 4 | "scripts": { 5 | "test": "npm run test:commonjs && npm run test:esm", 6 | "test:commonjs": "node test.cjs", 7 | "test:esm": "node test.mjs" 8 | }, 9 | "dependencies": { 10 | "js-xxhash": "file:.." 11 | }, 12 | "engines": { 13 | "node": ">=18.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | [sS]amples/ 3 | CHANGELOG.md 4 | cspell.json 5 | cspell.schema.json 6 | **/dist/** 7 | [tT]emp/ 8 | integration-tests/config/config.json 9 | integration-tests/repositories/*/*/** 10 | lerna.json 11 | tsconfig*.json 12 | packages/*/dist/ 13 | **/package-lock.json 14 | **/node_modules/** 15 | **/coverage/** 16 | **/*.map 17 | package-lock.json 18 | -------------------------------------------------------------------------------- /src/toUtf16.ts: -------------------------------------------------------------------------------- 1 | export function toUtf16(text: string): Uint8Array { 2 | const limit = text.length * 2; 3 | const bytes = new Uint8Array(limit); 4 | for (let i = 0; i < limit; i++) { 5 | const v = text.charCodeAt(i >> 1); 6 | bytes[i++] = v; 7 | bytes[i] = v >> 8; 8 | } 9 | return bytes; 10 | } 11 | 12 | export default toUtf16; 13 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [ 4 | "coverage/**", 5 | "dist/**", 6 | "node_modules/**", 7 | "tsconfig.json", 8 | "package.json", 9 | "package-lock.json" 10 | ], 11 | "words": [ 12 | "codeql", 13 | "cspellrc", 14 | "Dependabot", 15 | "streetsidesoftware", 16 | "xxhash", 17 | "xxhashjs" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.CRLF.txt] 12 | end_of_line = crlf 13 | 14 | [*.noEOL.txt] 15 | insert_final_newline = false 16 | 17 | [*.{yaml,yml}] 18 | indent_size = 2 19 | 20 | [{package*.json,lerna.json}] 21 | indent_size = 2 22 | 23 | [*.md] 24 | indent_size = 2 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist/esm", 6 | "lib": ["es2023", "dom"], 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "importHelpers": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "declarationMap": true, 14 | "rootDir": "./perf", 15 | "strict": true 16 | }, 17 | "include": ["perf"] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to NPM 2 | on: 3 | workflow_dispatch: 4 | # release: 5 | # types: [published] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v5 11 | # Setup .npmrc file to publish to npm 12 | - uses: actions/setup-node@v6 13 | with: 14 | node-version: "24.x" 15 | registry-url: "https://registry.npmjs.org" 16 | - run: npm ci 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist/esm", 6 | "target": "es2022", 7 | "lib": ["es2022", "dom"], 8 | "module": "NodeNext", 9 | "moduleResolution": "NodeNext", 10 | "esModuleInterop": true, 11 | "importHelpers": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "rootDir": "./src", 16 | "strict": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist/cjs", 6 | "target": "es2022", 7 | "lib": ["es2022", "dom"], 8 | "module": "CommonJS", 9 | "moduleResolution": "Node", 10 | "esModuleInterop": true, 11 | "importHelpers": false, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "rootDir": "./src", 16 | "strict": true 17 | }, 18 | "include": ["src"], 19 | "exclude": ["**/*.perf.*"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "target": "es2022", 6 | "lib": ["es2022", "dom"], 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "importHelpers": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "declarationMap": true, 14 | "rootDir": "./src", 15 | "strict": true, 16 | "verbatimModuleSyntax": true 17 | }, 18 | "files": [], 19 | "references": [{ "path": "./tsconfig.cjs.json" }, { "path": "./tsconfig.esm.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: streetsidesoftware 4 | # tidelift: "npm/cspell" 5 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 6 | # open_collective: # Replace with a single Open Collective username 7 | # ko_fi: # Replace with a single Ko-fi username 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v5 19 | with: 20 | # Make sure it goes back far enough to find where the branch split from main 21 | fetch-depth: 20 22 | 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v4 25 | with: 26 | languages: "javascript" 27 | 28 | - name: Perform CodeQL Analysis 29 | uses: github/codeql-action/analyze@v4 30 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import tseslint from 'typescript-eslint'; 4 | import { defineConfig } from 'eslint/config'; 5 | 6 | export default defineConfig([ 7 | { 8 | ignores: ['dist/**', '**/node_modules/**'], 9 | }, 10 | { 11 | files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], 12 | plugins: { js }, 13 | extends: ['js/recommended'], 14 | languageOptions: { globals: globals.node }, 15 | }, 16 | tseslint.configs.recommended, 17 | { 18 | files: ['**/*.cjs'], 19 | rules: { 20 | 'import/no-commonjs': 'off', 21 | '@typescript-eslint/no-require-imports': 'off', 22 | }, 23 | }, 24 | ]); 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | # Workflow files stored in the 15 | # default location of `.github/workflows` 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /src/toUtf16.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { toUtf16 } from './toUtf16.js'; 3 | 4 | describe('Validate toUtf16le', () => { 5 | getSamples().forEach((text) => 6 | it('test conversion is correct', () => { 7 | const expected = Buffer.from(text, 'utf16le'); 8 | expect(toString(toUtf16(text))).to.be.equal(text); 9 | expect(toUtf16(text)).to.be.deep.equal(expected); 10 | }), 11 | ); 12 | }); 13 | 14 | function toString(bytes: Uint8Array): string { 15 | const charCodes: number[] = []; 16 | 17 | for (let i = 0; i < bytes.length; i++) { 18 | charCodes.push(bytes[i] | (bytes[++i] << 8)); 19 | } 20 | 21 | return String.fromCharCode(...charCodes); 22 | } 23 | 24 | function getSamples() { 25 | return ['❤️', 'hello', 'a', 'á ä £ ™ ¢', '', 'नी ›', 'My text to convert 😊']; 26 | } 27 | -------------------------------------------------------------------------------- /test/test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | 4 | import { xxHash32 } from 'js-xxhash'; 5 | 6 | describe('xxHash32', () => { 7 | getSamples().forEach(({ s, e }) => 8 | test(`Test string: "${s.replace(/^(.{20}).*$/, '$1...')}"`, () => { 9 | const expected = e; 10 | const buffer = Buffer.from(s, 'utf8'); 11 | const actual = xxHash32(buffer).toString(16); 12 | assert.equal(actual, expected); 13 | }), 14 | ); 15 | }); 16 | 17 | function getSamples() { 18 | return [ 19 | { s: 'a', e: '550d7456' }, 20 | { s: 'ab', e: '4999fc53' }, 21 | { s: 'abc', e: '32d153ff' }, 22 | { s: 'abcd', e: 'a3643705' }, 23 | { s: 'abcde', e: '9738f19b' }, 24 | { s: 'ab'.repeat(10), e: '244fbf7c' }, 25 | { s: 'abc'.repeat(100), e: '55cad6be' }, 26 | { s: 'My text to hash 😊', e: 'af7fd356' }, 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /test/test.cjs: -------------------------------------------------------------------------------- 1 | const assert = require('node:assert'); 2 | const { describe, test } = require('node:test'); 3 | 4 | const { xxHash32 } = require('js-xxhash'); 5 | 6 | describe('xxHash32', () => { 7 | getSamples().forEach(({ s, e }) => 8 | test(`Test string: "${s.replace(/^(.{20}).*$/, '$1...')}"`, () => { 9 | const expected = e; 10 | const buffer = Buffer.from(s, 'utf8'); 11 | const actual = xxHash32(buffer).toString(16); 12 | assert.equal(actual, expected); 13 | }), 14 | ); 15 | }); 16 | 17 | function getSamples() { 18 | return [ 19 | { s: 'a', e: '550d7456' }, 20 | { s: 'ab', e: '4999fc53' }, 21 | { s: 'abc', e: '32d153ff' }, 22 | { s: 'abcd', e: 'a3643705' }, 23 | { s: 'abcde', e: '9738f19b' }, 24 | { s: 'ab'.repeat(10), e: '244fbf7c' }, 25 | { s: 'abc'.repeat(100), e: '55cad6be' }, 26 | { s: 'My text to hash 😊', e: 'af7fd356' }, 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/dist/index.js", 12 | "preLaunchTask": "tsc: build - tsconfig.json", 13 | "outFiles": [ 14 | "${workspaceFolder}/dist/**/*.js" 15 | ] 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Run Mocha Test Current File", 21 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 22 | "args": [ 23 | "--timeout","999999", 24 | "--colors", 25 | "--require", "ts-node/register", 26 | "${file}" 27 | ], 28 | "internalConsoleOptions": "openOnSessionStart" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jason Dent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | id-token: write 11 | 12 | name: release-please 13 | 14 | jobs: 15 | release-please: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: google-github-actions/release-please-action@v4 19 | id: release 20 | with: 21 | release-type: node 22 | 23 | # The logic below handles the npm publication: 24 | - uses: actions/checkout@v5 25 | # these if statements ensure that a publication only occurs when 26 | # a new release is created: 27 | if: ${{ steps.release.outputs.release_created }} 28 | - uses: actions/setup-node@v6 29 | with: 30 | node-version: 24.x 31 | registry-url: "https://registry.npmjs.org" 32 | if: ${{ steps.release.outputs.release_created }} 33 | - run: | 34 | npm ci 35 | npm run build 36 | if: ${{ steps.release.outputs.release_created }} 37 | - run: npm publish --provenance 38 | if: ${{ steps.release.outputs.release_created }} 39 | -------------------------------------------------------------------------------- /src/toUtf8.perf.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'perf-insight'; 2 | import { loremIpsum } from 'lorem-ipsum'; 3 | 4 | import { toUtf8, toUtf8_1, toUtf8_2, toUtf8_3 } from './toUtf8.js'; 5 | 6 | const wordCount = [10, 100, 1000, 10000, 100000]; 7 | 8 | for (const count of wordCount) { 9 | const innerTestIterations = Math.max(100 / count, 1); 10 | 11 | suite(`utf8-encoding-${count}`, `Evaluate encoding ${count} words.`, (test) => { 12 | const words = loremIpsum({ count, units: 'words' }); 13 | const iterations = innerTestIterations; 14 | const textEncoder = new TextEncoder(); 15 | 16 | const testMethod = (fn: (s: string) => unknown) => { 17 | for (let i = iterations; i > 0; --i) { 18 | fn(words); 19 | } 20 | }; 21 | 22 | test('toUtf8', () => testMethod(toUtf8)); 23 | test('toUtf8_1', () => testMethod(toUtf8_1)); 24 | test('toUtf8_2', () => testMethod(toUtf8_2)); 25 | test('toUtf8_3', () => testMethod(toUtf8_3)); 26 | test('Buffer.from', () => testMethod(Buffer.from)); 27 | test('TextEncoder', () => testMethod(textEncoder.encode.bind(textEncoder))); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | dist/ 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # Do not store the test package-lock.json because it is generated every time. 65 | test/package-lock.json 66 | -------------------------------------------------------------------------------- /src/toUtf8.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { toUtf8, toUtf8_1, toUtf8_2, toUtf8_3 } from './toUtf8.js'; 3 | 4 | import { loremIpsum as lorem } from 'lorem-ipsum'; 5 | 6 | const units: ('words' | 'sentences' | 'paragraphs')[] = ['words', 'sentences', 'paragraphs']; 7 | 8 | describe('Validate toUtf8', () => { 9 | getSamples().forEach((s) => 10 | it(`Test string: "${s.replace(/^(.{20}).*$/, '$1...')}"`, () => { 11 | const expected = Buffer.from(s, 'utf8'); 12 | const actual = toUtf8(s); 13 | expect(actual).to.deep.equal(expected); 14 | expect(toUtf8_1(s)).to.deep.equal(expected); 15 | expect(toUtf8_2(s)).to.deep.equal(expected); 16 | expect(toUtf8_3(s)).to.deep.equal(expected); 17 | }), 18 | ); 19 | 20 | for (let i = 0; i < 0; ++i) { 21 | const text = lorem({ 22 | count: Math.floor(Math.random() * 20), 23 | units: units[i % 3], 24 | }); 25 | it(`Test random string: "${text.slice(0, 30).replace(/^(.{20}).*$/, '$1...')}"`, () => { 26 | const expected = Buffer.from(text, 'utf8'); 27 | const actual = toUtf8(text); 28 | expect(actual).to.deep.equal(expected); 29 | expect(toUtf8_1(text)).to.deep.equal(expected); 30 | expect(toUtf8_2(text)).to.deep.equal(expected); 31 | expect(toUtf8_3(text)).to.deep.equal(expected); 32 | }); 33 | } 34 | }); 35 | 36 | function getSamples() { 37 | return ['a', 'á ä £ ™ ¢', '', 'नी ›', 'My text to hash 😊']; 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test-node-versions: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: 16 | - 20.x 17 | - 22.x 18 | - 24.x 19 | 20 | os: 21 | - ubuntu-latest 22 | 23 | include: 24 | - node-version: 24.x 25 | os: windows-latest 26 | - node-version: 24.x 27 | os: macos-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v5 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v6 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - run: npm ci 38 | 39 | - run: npm test 40 | 41 | # Ensure the repository is clean after build & test 42 | - run: git --no-pager diff --compact-summary --exit-code 43 | 44 | test-performance: 45 | runs-on: ${{ matrix.os }} 46 | 47 | strategy: 48 | matrix: 49 | node-version: 50 | - 24.x 51 | 52 | os: 53 | - ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v5 57 | 58 | - name: Use Node.js ${{ matrix.node-version }} 59 | uses: actions/setup-node@v6 60 | with: 61 | node-version: ${{ matrix.node-version }} 62 | 63 | - run: npm ci 64 | 65 | # test:integrations will setup the test environment, which contains some perf tests. 66 | - run: npm run test:integrations 67 | 68 | - run: npm run perf 69 | -------------------------------------------------------------------------------- /src/xxHash32.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { xxHash32 } from './xxHash32.js'; 3 | import { loremIpsum as lorem } from 'lorem-ipsum'; 4 | import * as xxh from 'xxhashjs'; 5 | 6 | const units: ('words' | 'sentences' | 'paragraphs')[] = ['words', 'sentences', 'paragraphs']; 7 | 8 | describe('Validate xxHash32', () => { 9 | getSamples().forEach(({ s, e }) => 10 | it(`Test string: "${s.replace(/^(.{20}).*$/, '$1...')}"`, () => { 11 | const expected = e; 12 | const buffer = Buffer.from(s, 'utf8'); 13 | const actual = xxHash32(buffer).toString(16); 14 | const expectedFromXxh = xxh.h32(buffer, 0).toString(16); 15 | expect(actual).to.be.equal(expected); 16 | expect(actual).to.be.equal(expectedFromXxh); 17 | }), 18 | ); 19 | 20 | for (let i = 0; i < 20; ++i) { 21 | const text = lorem({ 22 | count: Math.floor(Math.random() * 20), 23 | units: units[i % 3], 24 | }); 25 | it(`Test random string: "${text.slice(0, 30).replace(/^(.{20}).*$/, '$1...')}"`, () => { 26 | const actual = xxHash32(text).toString(16); 27 | const expected = xxh.h32(text, 0).toString(16); 28 | expect(actual).to.be.equal(expected); 29 | }); 30 | it(`Test random string (as buffer): "${text.slice(0, 30).replace(/^(.{20}).*$/, '$1...')}"`, () => { 31 | const buffer = Buffer.from(text, 'utf8'); 32 | const actual = xxHash32(buffer).toString(16); 33 | const expected = xxh.h32(buffer, 0).toString(16); 34 | expect(actual).to.be.equal(expected); 35 | }); 36 | } 37 | }); 38 | 39 | function getSamples() { 40 | return [ 41 | { s: 'a', e: '550d7456' }, 42 | { s: 'ab', e: '4999fc53' }, 43 | { s: 'abc', e: '32d153ff' }, 44 | { s: 'abcd', e: 'a3643705' }, 45 | { s: 'abcde', e: '9738f19b' }, 46 | { s: 'ab'.repeat(10), e: '244fbf7c' }, 47 | { s: 'abc'.repeat(100), e: '55cad6be' }, 48 | { s: 'My text to hash 😊', e: 'af7fd356' }, 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.0.1](https://github.com/Jason3S/xxhash/compare/v5.0.0...v5.0.1) (2025-11-19) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Fix Publish ([50818ae](https://github.com/Jason3S/xxhash/commit/50818aeed7c0630ad9ca69bb241e6ffc64283407)) 9 | 10 | ## [5.0.0](https://github.com/Jason3S/xxhash/compare/v4.0.0...v5.0.0) (2025-11-19) 11 | 12 | 13 | ### ⚠ BREAKING CHANGES 14 | 15 | * Require Node >= 20.x ([#652](https://github.com/Jason3S/xxhash/issues/652)) 16 | 17 | ### Features 18 | 19 | * Require Node >= 20.x ([#652](https://github.com/Jason3S/xxhash/issues/652)) ([82352e0](https://github.com/Jason3S/xxhash/commit/82352e01be0a7fc73c3ee3317c83379834012eaf)) 20 | 21 | ## [4.0.0](https://github.com/Jason3S/xxhash/compare/v3.0.1...v4.0.0) (2024-05-28) 22 | 23 | 24 | ### ⚠ BREAKING CHANGES 25 | 26 | * Use TextEncoder to convert strings ([#497](https://github.com/Jason3S/xxhash/issues/497)) 27 | 28 | ### Features 29 | 30 | * Use TextEncoder to convert strings ([#497](https://github.com/Jason3S/xxhash/issues/497)) ([e20b828](https://github.com/Jason3S/xxhash/commit/e20b828322c712cc4e857b2436a2dad9b43e8f49)) 31 | 32 | ## [3.0.1](https://github.com/Jason3S/xxhash/compare/v3.0.0...v3.0.1) (2024-01-15) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * Issue with CommonJS ([#453](https://github.com/Jason3S/xxhash/issues/453)) ([4d6e121](https://github.com/Jason3S/xxhash/commit/4d6e12111f860a2dbff832d13fa3647ddfeba32b)) 38 | 39 | ## [3.0.0](https://github.com/Jason3S/xxhash/compare/v2.0.0...v3.0.0) (2023-10-16) 40 | 41 | 42 | ### ⚠ BREAKING CHANGES 43 | 44 | * Export both CommonJS and ESM ([#417](https://github.com/Jason3S/xxhash/issues/417)) 45 | 46 | ### Features 47 | 48 | * Export both CommonJS and ESM ([#417](https://github.com/Jason3S/xxhash/issues/417)) ([e31e40c](https://github.com/Jason3S/xxhash/commit/e31e40c337f41d9c8199d4ff383b836f961710c8)) 49 | 50 | ## Changelog 51 | 52 | [2.0.0] 53 | **BREAKING:** dropping support for node 12 54 | 55 | The code should still run, but maintaining the tools to test on earlier versions of node is becoming too difficult to maintain. 56 | 57 | [1.0.3] 58 | - Do not publish source or map files. 59 | 60 | [1.0.2] 61 | - allow automatic conversion of strings to utf-8 before calculating the hash. 62 | 63 | [1.0.1] Initial release. 64 | -------------------------------------------------------------------------------- /test/perf/xxhash.perf.mts: -------------------------------------------------------------------------------- 1 | import { suite } from 'perf-insight'; 2 | import { xxHash32 } from 'js-xxhash'; 3 | import { h32 } from 'xxhashjs'; 4 | import { loremIpsum } from 'lorem-ipsum'; 5 | import xxhashWasm from 'xxhash-wasm'; 6 | import assert from 'node:assert'; 7 | 8 | const xxWasm = (await xxhashWasm()).h32; 9 | 10 | const wordCount = [10, 100, 1000, 10000, 100000]; 11 | 12 | suite('xxhash-short-strings', 'Evaluate xxhash performance with 10k short strings.', (test) => { 13 | const count = 10000; 14 | const words = loremIpsum({ count, units: 'words' }).split(' '); 15 | 16 | const testJsXxHash = () => { 17 | let h = 0; 18 | for (const word of words) { 19 | h += xxHash32(word, 42); 20 | } 21 | return h; 22 | }; 23 | 24 | const testXxhashjs = () => { 25 | let h = 0; 26 | for (const word of words) { 27 | h += h32(word, 42).toNumber(); 28 | } 29 | return h; 30 | }; 31 | 32 | const testXxhashWasm = () => { 33 | let h = 0; 34 | for (const word of words) { 35 | h += xxWasm(word, 42); 36 | } 37 | return h; 38 | }; 39 | 40 | const h0 = testJsXxHash(); 41 | const h1 = testXxhashjs(); 42 | const h2 = testXxhashWasm(); 43 | 44 | assert(h0 == h1); 45 | assert(h1 == h2); 46 | 47 | test('js-xxhash string', testJsXxHash); 48 | test('xxhashjs string', testXxhashjs); 49 | test('xxhash-wasm string', testXxhashWasm); 50 | }); 51 | 52 | // strings. 53 | for (const count of wordCount) { 54 | suite( 55 | `xxhash-string-words-${count}`, 56 | `Evaluate xxhash performance with a string of ${count} words.`, 57 | async (test) => { 58 | const words = loremIpsum({ count, units: 'words' }); 59 | const xxWasm = (await xxhashWasm()).h32; 60 | 61 | test('js-xxhash string', () => xxHash32(words, 42)); 62 | test('xxhashjs string', () => h32(words, 42)); 63 | test('xxhash-wasm string', () => xxWasm(words, 42)); 64 | }, 65 | ).setTimeout(count < 100 ? 1000 : 500); 66 | } 67 | 68 | // buffers. 69 | for (const count of wordCount) { 70 | suite( 71 | `xxhash-buffer-words-${count}`, 72 | `Evaluate xxhash performance with a buffer containing a string of ${count} words.`, 73 | async (test) => { 74 | const words = loremIpsum({ count, units: 'words' }); 75 | const buffer = Buffer.from(words); 76 | const xxWasm = (await xxhashWasm()).h32Raw; 77 | 78 | test('js-xxhash buffer', () => xxHash32(buffer, 42)); 79 | test('xxhashjs buffer', () => h32(buffer, 42)); 80 | test('xxhash-wasm buffer', () => xxWasm(buffer, 42)); 81 | }, 82 | ).setTimeout(count < 100 ? 1000 : 500); 83 | } 84 | -------------------------------------------------------------------------------- /src/toUtf8.ts: -------------------------------------------------------------------------------- 1 | export function toUtf8_1(text: string): Uint8Array { 2 | const bytes: number[] = []; 3 | const w = new Array(4); 4 | const h = [0x00, 0xc0, 0xe0, 0xf0]; 5 | const m = [0x7f, 0x3f, 0x3f, 0x3f]; 6 | const p = [0x00, 0x80, 0x80, 0x80]; 7 | for (const char of text) { 8 | const b = w; 9 | 10 | const cp = char.codePointAt(0)!; 11 | 12 | const n = 0 - (-(cp & 0xffffff80) >> 31) - (-(cp & 0xfffff800) >> 31) - (-(cp & 0xffff0000) >> 31); 13 | 14 | const z = m[n]; 15 | const y = p[n]; 16 | b[3] = y | (cp & z); 17 | b[2] = y | ((cp >>> 6) & z); 18 | b[1] = y | ((cp >>> 12) & z); 19 | b[0] = y | ((cp >>> 18) & z); 20 | const s = 3 - n; 21 | b[s] |= h[n]; 22 | 23 | Array.prototype.push.apply(bytes, b.slice(s)); 24 | } 25 | return new Uint8Array(bytes); 26 | } 27 | 28 | export function toUtf8_2(text: string): Uint8Array { 29 | const bytes: number[] = []; 30 | for (const char of text) { 31 | const cp = char.codePointAt(0)!; 32 | if (cp < 0x80) { 33 | bytes.push(cp); 34 | } else if (cp < 0x800) { 35 | bytes.push(0xc0 | ((cp >> 6) & 0x1f), 0x80 | (cp & 0x3f)); 36 | } else if (cp < 0x10000) { 37 | bytes.push(0xe0 | ((cp >> 12) & 0xf), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)); 38 | } else { 39 | bytes.push( 40 | 0xf0 | ((cp >> 18) & 0x7), 41 | 0x80 | ((cp >> 12) & 0x3f), 42 | 0x80 | ((cp >> 6) & 0x3f), 43 | 0x80 | (cp & 0x3f), 44 | ); 45 | } 46 | } 47 | return new Uint8Array(bytes); 48 | } 49 | 50 | /** 51 | * Convert text to UTF-8 byte array. 52 | * @param text text to be converted to utf-8 bytes 53 | * Note: this one seems to be the fastest based upon perf tests. 54 | */ 55 | export function toUtf8_3(text: string): Uint8Array { 56 | const bytes: number[] = []; 57 | for (let i = 0, n = text.length; i < n; ++i) { 58 | const c = text.charCodeAt(i); 59 | if (c < 0x80) { 60 | bytes.push(c); 61 | } else if (c < 0x800) { 62 | bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); 63 | } else if (c < 0xd800 || c >= 0xe000) { 64 | bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); 65 | } else { 66 | const cp = 0x10000 + (((c & 0x3ff) << 10) | (text.charCodeAt(++i) & 0x3ff)); 67 | bytes.push( 68 | 0xf0 | ((cp >> 18) & 0x7), 69 | 0x80 | ((cp >> 12) & 0x3f), 70 | 0x80 | ((cp >> 6) & 0x3f), 71 | 0x80 | (cp & 0x3f), 72 | ); 73 | } 74 | } 75 | return new Uint8Array(bytes); 76 | } 77 | 78 | export const toUtf8 = toUtf8_3; 79 | 80 | export default toUtf8; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-xxhash", 3 | "publishConfig": { 4 | "access": "public", 5 | "provenance": true 6 | }, 7 | "version": "5.0.1", 8 | "description": "Pure JS implementation of xxhash", 9 | "main": "dist/cjs/index.cjs", 10 | "types": "dist/cjs/index.d.cts", 11 | "type": "module", 12 | "exports": { 13 | ".": { 14 | "import": "./index.mjs", 15 | "types": "./dist/cjs/index.d.cts", 16 | "require": "./dist/cjs/index.cjs" 17 | } 18 | }, 19 | "files": [ 20 | "index.mjs", 21 | "dist/**", 22 | "!dist/cjs/**/*.js", 23 | "!dist/cjs/**/*.d.ts", 24 | "!**/*.map", 25 | "!**/*.perf.*", 26 | "!**/*.test.*", 27 | "!**/*.tsbuildinfo" 28 | ], 29 | "scripts": { 30 | "clean": "rimraf dist/**", 31 | "test": "npm run test:esm && npm run test:integrations", 32 | "test:esm": "mocha --recursive \"./dist/esm/**/*.test.js\"", 33 | "test:integrations": "cd test && npm i && npm test", 34 | "build": "tsc -b . && ts2mjs dist/cjs --cjs && npm run build:bundle", 35 | "build:bundle": "esbuild --bundle --platform=node --target=node18 --outfile=dist/cjs/index.cjs src/index.ts", 36 | "clean-build": "npm run clean && npm run build", 37 | "coverage": "NODE_ENV=test c8 -r lcov -r html npm run test-ts", 38 | "lint": "npm run install:test && eslint .", 39 | "install:test": "cd test && npm i", 40 | "prettier": "prettier . -w", 41 | "test-ts": "mocha --experimental-specifier-resolution=node --experimental-loader=ts-node/esm --require ts-node/register --recursive \"src/**/*.test.ts\"", 42 | "prepare": "npm run build", 43 | "perf": "perf-insight --register jiti/register --file \"**/*.perf.{mts,ts}\"", 44 | "insight:test": "perf-insight --register jiti/register --file \"test/**/*.perf.{mts,ts}\"", 45 | "insight:utf8": "perf-insight --register jiti/register --file \"**/toUtf8.perf.{mts,ts}\"", 46 | "ncu": "npx npm-check-updates", 47 | "update-packages": "npx npm-check-updates -t semver -u && rimraf node_modules package-lock.json && npm i", 48 | "watch": "tsc -b . -w" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/Jason3S/xxhash.git" 53 | }, 54 | "keywords": [ 55 | "xxhash", 56 | "javascript", 57 | "typescript" 58 | ], 59 | "author": "Jason Dent", 60 | "license": "MIT", 61 | "bugs": { 62 | "url": "https://github.com/Jason3S/xxhash/issues" 63 | }, 64 | "homepage": "https://github.com/Jason3S/xxhash#readme", 65 | "devDependencies": { 66 | "@eslint/js": "^9.39.1", 67 | "@tsconfig/node18": "^18.2.6", 68 | "@types/chai": "^5.2.3", 69 | "@types/mocha": "^10.0.10", 70 | "@types/node": "^18.19.130", 71 | "@types/xxhashjs": "^0.2.4", 72 | "c8": "^10.1.3", 73 | "chai": "^6.2.1", 74 | "esbuild": "^0.27.0", 75 | "eslint": "^9.39.1", 76 | "globals": "^16.5.0", 77 | "jiti": "^2.6.1", 78 | "lorem-ipsum": "^2.0.8", 79 | "mocha": "^11.7.5", 80 | "perf-insight": "^2.0.1", 81 | "prettier": "^3.6.2", 82 | "ts-node": "^10.9.2", 83 | "ts2mjs": "^4.0.0", 84 | "typescript": "^5.9.3", 85 | "typescript-eslint": "^8.47.0", 86 | "xxhash-wasm": "^1.1.0", 87 | "xxhashjs": "^0.2.2" 88 | }, 89 | "engines": { 90 | "node": ">=20.0.0" 91 | }, 92 | "packageManager": "npm@11.6.2+sha512.ee22b335fcbc95662cdf3ab8a053daf045d9cf9c6df6040d28965abb707512b2c16fa6c5eec049d34c74f78f390cebd14f697919eadb97756564d4f9eccc4954" 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-xxHash 2 | 3 | Pure Javascript / Typescript Implementation of [xxHash](http://cyan4973.github.io/xxHash/) 4 | 5 | This is an implementation for the 6 | [XXH32 Algorithm](https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md#xxh32-algorithm-description) 7 | A 64-bit version might come a bit later. 8 | 9 | ## Why another version 10 | 11 | - I needed a fast simple hash for short to medium sized strings. 12 | - It needed to be pure JS. 13 | 14 | ## Installation 15 | 16 | ``` 17 | npm install --save js-xxhash 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### Pure JS 23 | 24 | Internally it uses [TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) to convert strings to a UTF-8 `Uint8Array`s. 25 | 26 | ```typescript 27 | import { xxHash32 } from 'js-xxhash'; 28 | 29 | let seed = 0; 30 | let str = 'My text to hash 😊'; 31 | let hashNum = xxHash32(str, seed); 32 | console.log(hashNum.toString(16)); 33 | ``` 34 | 35 | Expected: 36 | 37 | ``` 38 | af7fd356 39 | ``` 40 | 41 | ### Node JS 42 | 43 | ```typescript 44 | import { xxHash32 } from 'js-xxhash'; 45 | 46 | let seed = 0; 47 | let str = 'My text to hash 😊'; 48 | let hashNum = xxHash32(str, seed); 49 | console.log(hashNum.toString(16)); 50 | ``` 51 | 52 | ### Browser 53 | 54 | ```typescript 55 | // Using a bundler 56 | import { xxHash32 } from 'js-xxhash'; 57 | // Using a CDN like jsDelivr 58 | import { xxHash32 } from 'https://cdn.jsdelivr.net/npm/js-xxhash@{version}/index.mjs'; 59 | 60 | let seed = 0; 61 | let str = 'My text to hash 😊'; 62 | let hashNum = xxHash32(str, seed); 63 | console.log(hashNum.toString(16)); 64 | ``` 65 | 66 | # Performance 67 | 68 | To evaluate performance this package was compared to: 69 | 70 | - [xxhashjs](https://www.npmjs.com/package/xxhashjs) 71 | - [xxhash-wasm](https://www.npmjs.com/package/xxhash-wasm) 72 | 73 | One average a lorem-ipsum "word" is between 5 and 6 characters. 74 | 75 | ## Performance for Strings 76 | 77 | ``` 78 | Running Perf Suite: xxhash-string-words-10 79 | Evaluate xxhash performance with a string of 10 words. 80 | ✔ js-xxhash string 731_148.14 ops/sec 81 | ✔ xxhashjs string 432_753.87 ops/sec 82 | ✔ xxhash-wasm string 3_381_907.91 ops/sec 83 | 84 | Running Perf Suite: xxhash-string-words-100 85 | Evaluate xxhash performance with a string of 100 words. 86 | ✔ js-xxhash string 420_458.19 ops/sec 87 | ✔ xxhashjs string 124_443.56 ops/sec 88 | ✔ xxhash-wasm string 2_289_457.63 ops/sec 89 | 90 | Running Perf Suite: xxhash-string-words-1000 91 | Evaluate xxhash performance with a string of 1000 words. 92 | ✔ js-xxhash string 74_861.33 ops/sec 93 | ✔ xxhashjs string 16_656.57 ops/sec 94 | ✔ xxhash-wasm string 729_339.20 ops/sec 95 | 96 | Running Perf Suite: xxhash-string-words-10000 97 | Evaluate xxhash performance with a string of 10000 words. 98 | ✔ js-xxhash string 6_293.40 ops/sec 99 | ✔ xxhashjs string 551.90 ops/sec 100 | ✔ xxhash-wasm string 90_170.30 ops/sec 101 | 102 | Running Perf Suite: xxhash-string-words-100000 103 | Evaluate xxhash performance with a string of 100000 words. 104 | ✔ js-xxhash string 709.30 ops/sec 105 | ✔ xxhashjs string 40.05 ops/sec 106 | ✔ xxhash-wasm string 8_093.17 ops/sec 107 | ``` 108 | 109 | ### Performance with a Uint8Array Buffer 110 | 111 | ``` 112 | Running Perf Suite: xxhash-buffer-words-10 113 | Evaluate xxhash performance with a buffer containing a string of 10 words. 114 | ✔ js-xxhash buffer 2_859_850.03 ops/sec 115 | ✔ xxhashjs buffer 699_053.22 ops/sec 116 | ✔ xxhash-wasm buffer 3_657_504.67 ops/sec 117 | 118 | Running Perf Suite: xxhash-buffer-words-100 119 | Evaluate xxhash performance with a buffer containing a string of 100 words. 120 | ✔ js-xxhash buffer 800_609.77 ops/sec 121 | ✔ xxhashjs buffer 402_424.91 ops/sec 122 | ✔ xxhash-wasm buffer 2_569_294.66 ops/sec 123 | 124 | Running Perf Suite: xxhash-buffer-words-1000 125 | Evaluate xxhash performance with a buffer containing a string of 1000 words. 126 | ✔ js-xxhash buffer 79_925.04 ops/sec 127 | ✔ xxhashjs buffer 55_568.13 ops/sec 128 | ✔ xxhash-wasm buffer 753_856.33 ops/sec 129 | 130 | Running Perf Suite: xxhash-buffer-words-10000 131 | Evaluate xxhash performance with a buffer containing a string of 10000 words. 132 | ✔ js-xxhash buffer 8_152.57 ops/sec 133 | ✔ xxhashjs buffer 6_046.82 ops/sec 134 | ✔ xxhash-wasm buffer 104_463.50 ops/sec 135 | 136 | Running Perf Suite: xxhash-buffer-words-100000 137 | Evaluate xxhash performance with a buffer containing a string of 100000 words. 138 | ✔ js-xxhash buffer 458.33 ops/sec 139 | ✔ xxhashjs buffer 602.90 ops/sec 140 | ✔ xxhash-wasm buffer 9_835.61 ops/sec 141 | ``` 142 | -------------------------------------------------------------------------------- /src/xxHash32.ts: -------------------------------------------------------------------------------- 1 | const PRIME32_1 = 2654435761; 2 | const PRIME32_2 = 2246822519; 3 | const PRIME32_3 = 3266489917; 4 | const PRIME32_4 = 668265263; 5 | const PRIME32_5 = 374761393; 6 | 7 | let encoder: TextEncoder | undefined; 8 | 9 | /** 10 | * 11 | * @param input - byte array or string 12 | * @param seed - optional seed (32-bit unsigned); 13 | */ 14 | export function xxHash32(input: Uint8Array | string, seed = 0): number { 15 | const buffer = typeof input === 'string' ? (encoder ??= new TextEncoder()).encode(input) : input; 16 | const b = buffer; 17 | 18 | /* 19 | Step 1. Initialize internal accumulators 20 | Each accumulator gets an initial value based on optional seed input. Since the seed is optional, it can be 0. 21 | 22 | ``` 23 | u32 acc1 = seed + PRIME32_1 + PRIME32_2; 24 | u32 acc2 = seed + PRIME32_2; 25 | u32 acc3 = seed + 0; 26 | u32 acc4 = seed - PRIME32_1; 27 | ``` 28 | Special case : input is less than 16 bytes 29 | When input is too small (< 16 bytes), the algorithm will not process any stripe. Consequently, it will not 30 | make use of parallel accumulators. 31 | 32 | In which case, a simplified initialization is performed, using a single accumulator : 33 | 34 | u32 acc = seed + PRIME32_5; 35 | The algorithm then proceeds directly to step 4. 36 | */ 37 | 38 | let acc = (seed + PRIME32_5) & 0xffffffff; 39 | let offset = 0; 40 | 41 | if (b.length >= 16) { 42 | const accN = [ 43 | (seed + PRIME32_1 + PRIME32_2) & 0xffffffff, 44 | (seed + PRIME32_2) & 0xffffffff, 45 | (seed + 0) & 0xffffffff, 46 | (seed - PRIME32_1) & 0xffffffff, 47 | ]; 48 | 49 | /* 50 | Step 2. Process stripes 51 | A stripe is a contiguous segment of 16 bytes. It is evenly divided into 4 lanes, of 4 bytes each. 52 | The first lane is used to update accumulator 1, the second lane is used to update accumulator 2, and so on. 53 | 54 | Each lane read its associated 32-bit value using little-endian convention. 55 | 56 | For each {lane, accumulator}, the update process is called a round, and applies the following formula : 57 | 58 | ``` 59 | accN = accN + (laneN * PRIME32_2); 60 | accN = accN <<< 13; 61 | accN = accN * PRIME32_1; 62 | ``` 63 | 64 | This shuffles the bits so that any bit from input lane impacts several bits in output accumulator. 65 | All operations are performed modulo 2^32. 66 | 67 | Input is consumed one full stripe at a time. Step 2 is looped as many times as necessary to consume 68 | the whole input, except the last remaining bytes which cannot form a stripe (< 16 bytes). When that 69 | happens, move to step 3. 70 | */ 71 | 72 | const b = buffer; 73 | const limit = b.length - 16; 74 | let lane = 0; 75 | for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) { 76 | const i = offset; 77 | const laneN0 = b[i + 0] + (b[i + 1] << 8); 78 | const laneN1 = b[i + 2] + (b[i + 3] << 8); 79 | const laneNP = laneN0 * PRIME32_2 + ((laneN1 * PRIME32_2) << 16); 80 | let acc = (accN[lane] + laneNP) & 0xffffffff; 81 | acc = (acc << 13) | (acc >>> 19); 82 | const acc0 = acc & 0xffff; 83 | const acc1 = acc >>> 16; 84 | accN[lane] = (acc0 * PRIME32_1 + ((acc1 * PRIME32_1) << 16)) & 0xffffffff; 85 | lane = (lane + 1) & 0x3; 86 | } 87 | 88 | /* 89 | Step 3. Accumulator convergence 90 | All 4 lane accumulators from previous steps are merged to produce a single remaining accumulator 91 | of same width (32-bit). The associated formula is as follows : 92 | 93 | ``` 94 | acc = (acc1 <<< 1) + (acc2 <<< 7) + (acc3 <<< 12) + (acc4 <<< 18); 95 | ``` 96 | */ 97 | acc = 98 | (((accN[0] << 1) | (accN[0] >>> 31)) + 99 | ((accN[1] << 7) | (accN[1] >>> 25)) + 100 | ((accN[2] << 12) | (accN[2] >>> 20)) + 101 | ((accN[3] << 18) | (accN[3] >>> 14))) & 102 | 0xffffffff; 103 | } 104 | 105 | /* 106 | Step 4. Add input length 107 | The input total length is presumed known at this stage. This step is just about adding the length to 108 | accumulator, so that it participates to final mixing. 109 | 110 | ``` 111 | acc = acc + (u32)inputLength; 112 | ``` 113 | */ 114 | acc = (acc + buffer.length) & 0xffffffff; 115 | 116 | /* 117 | Step 5. Consume remaining input 118 | There may be up to 15 bytes remaining to consume from the input. The final stage will digest them according 119 | to following pseudo-code : 120 | ``` 121 | while (remainingLength >= 4) { 122 | lane = read_32bit_little_endian(input_ptr); 123 | acc = acc + lane * PRIME32_3; 124 | acc = (acc <<< 17) * PRIME32_4; 125 | input_ptr += 4; remainingLength -= 4; 126 | } 127 | ``` 128 | This process ensures that all input bytes are present in the final mix. 129 | */ 130 | 131 | const limit = buffer.length - 4; 132 | for (; offset <= limit; offset += 4) { 133 | const i = offset; 134 | const laneN0 = b[i + 0] + (b[i + 1] << 8); 135 | const laneN1 = b[i + 2] + (b[i + 3] << 8); 136 | const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16); 137 | acc = (acc + laneP) & 0xffffffff; 138 | acc = (acc << 17) | (acc >>> 15); 139 | acc = ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) & 0xffffffff; 140 | } 141 | 142 | /* 143 | ``` 144 | while (remainingLength >= 1) { 145 | lane = read_byte(input_ptr); 146 | acc = acc + lane * PRIME32_5; 147 | acc = (acc <<< 11) * PRIME32_1; 148 | input_ptr += 1; remainingLength -= 1; 149 | } 150 | ``` 151 | */ 152 | 153 | for (; offset < b.length; ++offset) { 154 | const lane = b[offset]; 155 | acc = acc + lane * PRIME32_5; 156 | acc = (acc << 11) | (acc >>> 21); 157 | acc = ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) & 0xffffffff; 158 | } 159 | 160 | /* 161 | Step 6. Final mix (avalanche) 162 | The final mix ensures that all input bits have a chance to impact any bit in the output digest, 163 | resulting in an unbiased distribution. This is also called avalanche effect. 164 | ``` 165 | acc = acc xor (acc >> 15); 166 | acc = acc * PRIME32_2; 167 | acc = acc xor (acc >> 13); 168 | acc = acc * PRIME32_3; 169 | acc = acc xor (acc >> 16); 170 | ``` 171 | */ 172 | 173 | acc = acc ^ (acc >>> 15); 174 | acc = (((acc & 0xffff) * PRIME32_2) & 0xffffffff) + (((acc >>> 16) * PRIME32_2) << 16); 175 | acc = acc ^ (acc >>> 13); 176 | acc = (((acc & 0xffff) * PRIME32_3) & 0xffffffff) + (((acc >>> 16) * PRIME32_3) << 16); 177 | acc = acc ^ (acc >>> 16); 178 | 179 | // turn any negatives back into a positive number; 180 | return acc < 0 ? acc + 4294967296 : acc; 181 | } 182 | --------------------------------------------------------------------------------