├── .changeset ├── README.md └── config.json ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── extras ├── benchmark │ ├── cosine_benchmark.md │ └── euclidean_benchmark.md └── media │ └── image.png ├── package-lock.json ├── package.json ├── src ├── errors.ts ├── heap.ts ├── hnsw.ts ├── index.ts ├── metric.ts ├── node.ts ├── options.ts └── store.ts ├── test └── hnsw.test.ts ├── tsconfig.json └── tsup.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM publish 2 | 3 | on: 4 | push: 5 | branches: master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: "20" 15 | - run: npm ci 16 | - run: npm test 17 | - uses: JS-DevTools/npm-publish@v3 18 | with: 19 | token: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | build/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /build/ 4 | /tmp/ 5 | 6 | # package 7 | /media/ 8 | /docs/ 9 | /extras/ 10 | /benchmarks/ 11 | 12 | # dependencies 13 | /bower_components/ 14 | 15 | # misc 16 | /.bowerrc 17 | /.editorconfig 18 | /.ember-cli 19 | /.env* 20 | /.eslintcache 21 | /.eslintignore 22 | /.eslintrc.js 23 | /.git/ 24 | /.github/ 25 | /.gitignore 26 | /.prettierignore 27 | /.prettierrc.js 28 | /.template-lintrc.js 29 | /.travis.yml 30 | /.watchmanconfig 31 | /bower.json 32 | /config/ember-try.js 33 | /CONTRIBUTING.md 34 | /ember-cli-build.js 35 | /testem.js 36 | /tests/ 37 | /yarn-error.log 38 | /yarn.lock 39 | .gitkeep 40 | 41 | # ember-try 42 | /.node_modules.ember-try/ 43 | /bower.json.ember-try 44 | /npm-shrinkwrap.json.ember-try 45 | /package.json.ember-try 46 | /package-lock.json.ember-try 47 | /yarn.lock.ember-try 48 | 49 | # TS sources and compiled tests 50 | /ts/ 51 | /js/tests/ 52 | /test-fixtures/ 53 | 54 | # custom 55 | /commitlint.config.js 56 | /.vscode/ 57 | /renovate.json 58 | /CODE_OF_CONDUCT.md 59 | /rfcs 60 | /*.sh 61 | /.gitbook.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "printWidth": 80, 4 | "editor.formatOnSave": true, 5 | "proseWrap": "always", 6 | "tabWidth": 4, 7 | "requireConfig": false, 8 | "useTabs": false, 9 | "trailingComma": "none", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "semi": true 13 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2028 Tikerbird Authors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

TinkerBird

2 | 3 | TinkerBird is a browser native vector database designed for efficient storage and 4 | retrieval of high-dimensional vectors (embeddings). It's query engine, written in 5 | TypeScript, leverages HNSW (Hierarchical Navigable Small World) indexes for fast 6 | vector retrieval. The storage layer utilizes IndexedDB, which could be extended 7 | with an lru-cache. 8 | 9 | By co-locating data and embeddings, Tikerbird eliminates the roundtrip and reduces 10 | reliance on server-side interactions for vector search workloads. With Tinkerbird, 11 | sensitive data remains local, thus benefiting from vector search, without the associated cost, 12 | compliance and security risks. 13 | 14 | TinkerBird uses IndexedDB as it's storage layer, which in turn builds upon Blobs 15 | and LevelDB storage systems. By using Indexeddb, it benefits from IndexedDB's 16 | adoption, stability and familiarity as a native choice for offline first 17 | workflows. 18 | 19 | ## Examples 20 | 21 | Here's a sample app built using TinkerBird. Check out [Tinkerboard](https://tinkerboard.vercel.app/) and [Source](https://github.com/wizenheimer/tinkerboard). 22 | 23 | ## Contributing 24 | 25 | Feel free to contribute to TinkerBird by sending us your suggestions, bug 26 | reports, or cat videos. Contributions are what make the open source community 27 | such an amazing place to be learn, inspire, and create. Any contributions you 28 | make are **greatly appreciated**. 29 | 30 | ## License 31 | 32 | Distributed under the MIT License. See [LICENSE](LICENSE.md) for more information. 33 | TinkerBird is provided "as is" and comes with absolutely no guarantees. We take 34 | no responsibility for irrelevant searches, confused users, or existential crises 35 | induced by unpredictable results. If it breaks, well, that's your problem now! jk. 36 | 37 | 38 | ## References 39 | 40 | - [ANN-Benchmarks](https://github.com/erikbern/ann-benchmarks) 41 | - [Skip Lists: A Probabilistic Alternative to Balanced Trees](https://15721.courses.cs.cmu.edu/spring2018/papers/08-oltpindexes1/pugh-skiplists-cacm1990.pdf) 42 | - [Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small Worlds](https://arxiv.org/abs/1603.09320) 43 | - [Scalable Distributed Algorithm for Approximate Nearest Neighbor Search Problem in High-Dimensional General Metric Spaces](https://www.iiis.org/CDs2011/CD2011IDI/ICTA_2011/PapersPdf/CT175ON.pdf) 44 | - [A Comparative Study on Hierarchical Navigable Small World Graphs](https://deepai.org/publication/a-comparative-study-on-hierarchical-navigable-small-world-graphs) 45 | - [HNSW: Hierarchical Navigable Small World graphs](https://proceedings.mlr.press/v119/prokhorenkova20a/prokhorenkova20a.pdf) 46 | - [HNSW Graphs](https://github.com/deepfates/hnsw/) 47 | - [Hierarchical Navigable Small World graphs in FAISS](https://github.com/facebookresearch/faiss/blob/main/faiss/impl/HNSW.cpp) 48 | - [A Comparative Study on Hierarchical Navigable Small World Graphs](https://escholarship.org/content/qt1rp889r9/qt1rp889r9_noSplash_7071690a1d8a4ee71eb95432887d3a8e.pdf) 49 | - [Hierarchical Navigable Small World (HNSW) for ApproximateNearest Neighbor Search](https://towardsdatascience.com/similarity-search-part-4-hierarchical-navigable-small-world-hnsw-2aad4fe87d37) 50 | - [Hierarchical Navigable Small Worlds](https://srivatssan.medium.com/hierarchical-navigable-small-worlds-d44d39d91f4b) 51 | -------------------------------------------------------------------------------- /extras/benchmark/cosine_benchmark.md: -------------------------------------------------------------------------------- 1 | | dataSize | dimension | k | metric | IndexBuildTime (ms) | QueryTime (ms) | Query Results | 2 | | -------- | --------- | --- | ------ | ------------------- | -------------- | ------------------------------- | 3 | | 100 | 128 | 3 | cosine | 6.842 | 1.054 | [{ id: 1, score: 0.7716 }, ...] | 4 | | 100 | 128 | 3 | cosine | 3.636 | 0.647 | [{ id: 1, score: 0.7482 }, ...] | 5 | | 100 | 128 | 3 | cosine | 10.428 | 0.239 | [{ id: 1, score: 0.7576 }, ...] | 6 | | 100 | 128 | 3 | cosine | 2.981 | 0.164 | [{ id: 1, score: 0.7160 }, ...] | 7 | | 100 | 128 | 3 | cosine | 4.311 | 0.574 | [{ id: 1, score: 0.7694 }, ...] | 8 | | 100 | 128 | 3 | cosine | 2.441 | 0.513 | [{ id: 1, score: 0.7632 }, ...] | 9 | | 100 | 128 | 3 | cosine | 2.230 | 0.367 | [{ id: 1, score: 0.7515 }, ...] | 10 | | 100 | 128 | 3 | cosine | 2.778 | 0.266 | [{ id: 1, score: 0.7016 }, ...] | 11 | | 100 | 128 | 3 | cosine | 1.885 | 0.299 | [{ id: 1, score: 0.7365 }, ...] | 12 | | 1000 | 128 | 3 | cosine | 34.440 | 0.308 | [{ id: 1, score: 0.7283 }, ...] | 13 | | 1000 | 128 | 3 | cosine | 36.705 | 0.363 | [{ id: 1, score: 0.7562 }, ...] | 14 | | 1000 | 128 | 3 | cosine | 26.751 | 0.376 | [{ id: 1, score: 0.7100 }, ...] | 15 | | 1000 | 128 | 3 | cosine | 27.724 | 0.259 | [{ id: 1, score: 0.7576 }, ...] | 16 | | 1000 | 128 | 3 | cosine | 25.132 | 0.280 | [{ id: 1, score: 0.7448 }, ...] | 17 | | 1000 | 128 | 3 | cosine | 30.000 | 0.440 | [{ id: 1, score: 0.7552 }, ...] | 18 | | 1000 | 128 | 3 | cosine | 24.165 | 1.128 | [{ id: 1, score: 0.7438 }, ...] | 19 | | 1000 | 128 | 3 | cosine | 22.574 | 1.351 | [{ id: 1, score: 0.7906 }, ...] | 20 | | 1000 | 128 | 3 | cosine | 23.595 | 0.310 | [{ id: 1, score: 0.6802 }, ...] | 21 | | 10000 | 128 | 3 | cosine | 274.783 | 0.503 | [{ id: 1, score: 0.7437 }, ...] | 22 | | 10000 | 128 | 3 | cosine | 302.001 | 0.869 | [{ id: 1, score: 0.7232 }, ...] | 23 | | 10000 | 128 | 3 | cosine | 263.236 | 0.880 | [{ id: 1, score: 0.7870 }, ...] | 24 | | 10000 | 128 | 3 | cosine | 273.003 | 0.382 | [{ id: 1, score: 0.6975 }, ...] | 25 | | 10000 | 128 | 3 | cosine | 256.867 | 0.462 | [{ id: 1, score: 0.7763 }, ...] | 26 | | 10000 | 128 | 3 | cosine | 274.949 | 0.352 | [{ id: 1, score: 0.7684 }, ...] | 27 | | 10000 | 128 | 3 | cosine | 257.376 | 0.419 | [{ id: 1, score: 0.7869 }, ...] | 28 | | 10000 | 128 | 3 | cosine | 298.475 | 0.370 | [{ id: 1, score: 0.7494 }, ...] | 29 | | 10000 | 128 | 3 | cosine | 268.296 | 0.367 | [{ id: 1, score: 0.7659 }, ...] | 30 | -------------------------------------------------------------------------------- /extras/benchmark/euclidean_benchmark.md: -------------------------------------------------------------------------------- 1 | | dataSize | dimension | k | metric | IndexBuildTime (ms) | QueryTime (ms) | Query Results | 2 | | -------- | --------- | --- | --------- | ------------------- | -------------- | ------------------------------- | 3 | | 100 | 128 | 3 | euclidean | 7.381 | 0.696 | [{ id: 1, score: 0.7594 }, ...] | 4 | | 100 | 128 | 3 | euclidean | 3.963 | 0.518 | [{ id: 1, score: 0.7343 }, ...] | 5 | | 100 | 128 | 3 | euclidean | 4.068 | 0.293 | [{ id: 1, score: 0.7306 }, ...] | 6 | | 100 | 128 | 3 | euclidean | 3.847 | 0.312 | [{ id: 1, score: 0.7695 }, ...] | 7 | | 100 | 128 | 3 | euclidean | 3.036 | 0.325 | [{ id: 1, score: 0.7425 }, ...] | 8 | | 100 | 128 | 3 | euclidean | 2.592 | 0.284 | [{ id: 1, score: 0.7275 }, ...] | 9 | | 100 | 128 | 3 | euclidean | 3.755 | 0.164 | [{ id: 1, score: 0.6889 }, ...] | 10 | | 100 | 128 | 3 | euclidean | 3.44 | 0.33 | [{ id: 1, score: 0.7595 }, ...] | 11 | | 100 | 128 | 3 | euclidean | 2.839 | 0.157 | [{ id: 1, score: 0.7217 }, ...] | 12 | | 1000 | 128 | 3 | euclidean | 28.871 | 0.474 | [{ id: 1, score: 0.7703 }, ...] | 13 | | 1000 | 128 | 3 | euclidean | 27.566 | 0.679 | [{ id: 1, score: 0.8053 }, ...] | 14 | | 1000 | 128 | 3 | euclidean | 27.909 | 0.32 | [{ id: 1, score: 0.6920 }, ...] | 15 | | 1000 | 128 | 3 | euclidean | 27.872 | 0.578 | [{ id: 1, score: 0.7774 }, ...] | 16 | | 1000 | 128 | 3 | euclidean | 24.352 | 0.432 | [{ id: 1, score: 0.7547 }, ...] | 17 | | 1000 | 128 | 3 | euclidean | 25.726 | 0.35 | [{ id: 1, score: 0.7362 }, ...] | 18 | | 1000 | 128 | 3 | euclidean | 27.978 | 0.484 | [{ id: 1, score: 0.7153 }, ...] | 19 | | 1000 | 128 | 3 | euclidean | 24.901 | 0.42 | [{ id: 1, score: 0.7322 }, ...] | 20 | | 1000 | 128 | 3 | euclidean | 26.622 | 0.556 | [{ id: 1, score: 0.7553 }, ...] | 21 | | 10000 | 128 | 3 | euclidean | 282.583 | 0.544 | [{ id: 1, score: 0.7574 }, ...] | 22 | | 10000 | 128 | 3 | euclidean | 283.921 | 0.406 | [{ id: 1, score: 0.7443 }, ...] | 23 | | 10000 | 128 | 3 | euclidean | 270.122 | 0.325 | [{ id: 1, score: 0.7555 }, ...] | 24 | | 10000 | 128 | 3 | euclidean | 246.290 | 0.346 | [{ id: 1, score: 0.7630 }, ...] | 25 | | 10000 | 128 | 3 | euclidean | 246.418 | 0.763 | [{ id: 1, score: 0.7176 }, ...] | 26 | | 10000 | 128 | 3 | euclidean | 271.542 | 0.448 | [{ id: 1, score: 0.7823 }, ...] | 27 | | 10000 | 128 | 3 | euclidean | 265.753 | 0.399 | [{ id: 1, score: 0.7165 }, ...] | 28 | | 10000 | 128 | 3 | euclidean | 320.801 | 0.423 | [{ id: 1, score: 0.7865 }, ...] | 29 | | 10000 | 128 | 3 | euclidean | 254.345 | 0.452 | [{ id: 1, score: 0.8285 }, ...] | 30 | -------------------------------------------------------------------------------- /extras/media/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizenheimer/tinkerbird/f22787fae0c8a2b1959ee33278f8b18b99ec7532/extras/media/image.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinkerbird", 3 | "version": "0.0.7", 4 | "description": "Chrome Native Vector Database | Client Side Vector Search", 5 | "private": false, 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "dev:test": "vitest", 14 | "test": "vitest run", 15 | "build": "tsup src/*.ts", 16 | "lint": "tsc", 17 | "release": "npm run lint && npm run test && npm run build && changeset publish" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "github.com/wizenheimer/tinkerbird" 22 | }, 23 | "author": "wizenheimer", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@changesets/cli": "^2.27.1", 27 | "@faker-js/faker": "^8.3.1", 28 | "tsup": "^8.0.1", 29 | "vitest": "^1.0.2" 30 | }, 31 | "dependencies": { 32 | "idb": "^8.0.0" 33 | } 34 | } -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export const VectorStoreUnintialized = new Error( 2 | "Vector Store is uninitialized." 3 | ); 4 | export const VectorStoreIndexMissing = new Error( 5 | "Vector Store Index is missing." 6 | ); 7 | export const VectorStoreIndexPurgeFailed = new Error( 8 | "Vector Store Index can't be deleted." 9 | ); 10 | export const VectorStoreRecreationFailed = new Error( 11 | "Vector Store Index can't be recreated." 12 | ); 13 | -------------------------------------------------------------------------------- /src/heap.ts: -------------------------------------------------------------------------------- 1 | export class Heap { 2 | private items: T[] = []; 3 | constructor(private comparator: (a: T, b: T) => number) {} 4 | 5 | push(item: T): void { 6 | let i = 0; 7 | while ( 8 | i < this.items.length && 9 | this.comparator(item, this.items[i]) > 0 10 | ) { 11 | i += 1; 12 | } 13 | this.items.splice(i, 0, item); 14 | } 15 | 16 | pop(): T | undefined { 17 | return this.items.shift(); 18 | } 19 | 20 | isEmpty(): boolean { 21 | return this.items.length === 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/hnsw.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SimilarityMetric, 3 | cosineSimilarity, 4 | euclideanSimilarity 5 | } from "./metric"; 6 | 7 | import { Content, Node, Embedding } from "./node"; 8 | import { Heap } from "./heap"; 9 | 10 | export type vectorReducer = (a: Embedding, b: Embedding) => number; 11 | export type vectorTransformer = (a: Embedding, b: Embedding) => Embedding; 12 | export type vectorResult = { 13 | id: number; 14 | content: Content; 15 | embedding: Embedding; 16 | score: number; 17 | }[]; 18 | 19 | const incorrectDimension = new Error("Invalid Vector Dimension"); 20 | 21 | export class HNSW { 22 | metric: SimilarityMetric; // similarity metric 23 | similarityFunction: vectorReducer; // similarity function 24 | d: number | null = null; // embedding dimension 25 | M: number; // maximum neighbor count 26 | efConstruction: number; // effervescence coefficient 27 | entryPointId: number; // id of entry node 28 | nodes: Map; // mapping of [id: Node] 29 | probs: number[]; // probabilities for each level 30 | levelMax: number; // maximum level of the graph 31 | 32 | constructor( 33 | M = 16, 34 | efConstruction = 200, 35 | d: number | null = null, 36 | metric: SimilarityMetric = SimilarityMetric.cosine 37 | ) { 38 | this.metric = metric; 39 | this.similarityFunction = this.getSimilarityFunction(); 40 | this.d = d; 41 | this.M = M; 42 | this.efConstruction = efConstruction; 43 | this.entryPointId = -1; 44 | this.nodes = new Map(); 45 | this.probs = this.getProbDistribution(); 46 | this.levelMax = this.probs.length - 1; 47 | } 48 | 49 | private getSimilarityFunction() { 50 | return this.metric === SimilarityMetric.cosine 51 | ? cosineSimilarity 52 | : euclideanSimilarity; 53 | } 54 | 55 | // figure out the probability distribution along the level of the layers 56 | private getProbDistribution(): number[] { 57 | const levelMult = 1 / Math.log(this.M); 58 | let probs = [] as number[], 59 | level = 0; 60 | while (true) { 61 | const prob = 62 | Math.exp(-level / levelMult) * (1 - Math.exp(-1 / levelMult)); 63 | if (prob < 1e-9) break; 64 | probs.push(prob); 65 | level++; 66 | } 67 | return probs; 68 | } 69 | 70 | // perform vector search on the index 71 | query(target: Embedding, k: number = 3): vectorResult { 72 | const result: vectorResult = []; // storing the query result 73 | const visited: Set = new Set(); // de duplicate candidate search 74 | 75 | // main a heap of candidates that are ordered by similarity 76 | const orderBySimilarity = (aID: number, bID: number) => { 77 | const aNode = this.nodes.get(aID)!; 78 | const bNode = this.nodes.get(bID)!; 79 | return ( 80 | this.similarityFunction(target, bNode.embedding) - 81 | this.similarityFunction(target, aNode.embedding) 82 | ); 83 | }; 84 | const candidates = new Heap(orderBySimilarity); 85 | candidates.push(this.entryPointId); 86 | 87 | let level = this.levelMax; 88 | // do until we have required result 89 | while (!candidates.isEmpty() && result.length < k) { 90 | const currID = candidates.pop()!; 91 | if (visited.has(currID)) continue; 92 | 93 | visited.add(currID); 94 | const currNode = this.nodes.get(currID)!; 95 | const currSimilarity = this.similarityFunction( 96 | currNode.embedding, 97 | target 98 | ); 99 | if (currSimilarity > 0) { 100 | result.push({ 101 | id: currID, 102 | content: currNode.content, 103 | embedding: currNode.embedding, 104 | score: currSimilarity 105 | }); 106 | } 107 | 108 | // no more levels left to explore 109 | if (currNode.level === 0) { 110 | continue; 111 | } 112 | 113 | // explore the neighbors of candidates from each level 114 | level = Math.min(level, currNode.level - 1); 115 | for (let i = level; i >= 0; i -= 1) { 116 | const neighbors = currNode.neighbors[i]; 117 | for (const neighborId of neighbors) { 118 | if (!visited.has(neighborId)) { 119 | candidates.push(neighborId); 120 | } 121 | } 122 | } 123 | } 124 | 125 | // pick the top k candidates from the result 126 | return result.slice(0, k); 127 | } 128 | 129 | // stochastically pick a level, higher the probability greater the chances of getting picked 130 | private determineLevel(): number { 131 | let r = Math.random(); 132 | this.probs.forEach((pLevel, index) => { 133 | if (r < pLevel) return index; 134 | r -= pLevel; 135 | }); 136 | return this.probs.length - 1; 137 | } 138 | 139 | async buildIndex( 140 | data: { id: number; embedding: Embedding; content: Content }[] 141 | ) { 142 | // reset existing index 143 | this.nodes.clear(); 144 | this.levelMax = 0; 145 | this.entryPointId = -1; 146 | 147 | // add current points into index 148 | data.forEach(async (item) => { 149 | await this.addVector(item.id, item.embedding, item.content); 150 | }); 151 | } 152 | 153 | async addVector(id: number, embedding: Embedding, content: Content) { 154 | // check and initialize dimensions if needed 155 | if (this.d === null) { 156 | this.d = embedding.length; 157 | } else if (embedding.length !== this.d) { 158 | throw incorrectDimension; 159 | } 160 | 161 | // create and add newNode into index 162 | const newNode = new Node( 163 | id, 164 | this.determineLevel(), 165 | embedding, 166 | this.M, 167 | content 168 | ); 169 | this.nodes.set(id, newNode); 170 | 171 | // add node to index 172 | await this.addNode(newNode); 173 | } 174 | 175 | async addNode(targetNode: Node) { 176 | // incase this is the first node, set this as entry point and back out 177 | if (this.entryPointId === -1) { 178 | this.entryPointId = targetNode.id; 179 | return; 180 | } 181 | 182 | // start from the entry point 183 | // find the closest node to the target node 184 | let currNode = this.nodes.get(this.entryPointId)!; 185 | let closestNode = currNode; 186 | 187 | for (let level = this.levelMax; level >= 0; level -= 1) { 188 | // find the node closest to target node in the level, excluding the currentNode 189 | let nextNode = this.findNextNode(currNode, targetNode, level); 190 | // incase there's a nextNode, check to see if it's closer than the closestNode 191 | if (nextNode) { 192 | const similarity = this.similarityFunction( 193 | targetNode.embedding, 194 | nextNode.embedding 195 | ); 196 | if ( 197 | similarity > 198 | this.similarityFunction( 199 | targetNode.embedding, 200 | closestNode.embedding 201 | ) 202 | ) { 203 | currNode = nextNode; 204 | closestNode = currNode; 205 | } else { 206 | break; 207 | } 208 | } 209 | } 210 | 211 | // find the level that's common to closest node and target node 212 | // highest level at which both are going to have neighbors 213 | const commonLevel = Math.min(targetNode.level, closestNode.level); 214 | for (let level = 0; level <= commonLevel; level += 1) { 215 | // update the neighborhood, such that both nodes share common neighbors upto a common level 216 | const addToNeighborhood = (srcNode: Node, trgNode: Node) => { 217 | // filter out sentinel ids 218 | srcNode.neighbors[level] = srcNode.neighbors[level].filter( 219 | (id) => id !== -1 220 | ); 221 | // add trgNode to the neighbor 222 | srcNode.neighbors[level].push(trgNode.id); 223 | // incase the max neighbor are exceeded, remove the farthest 224 | if (srcNode.neighbors[level].length > this.M) { 225 | srcNode.neighbors[level].pop(); 226 | } 227 | }; 228 | addToNeighborhood(closestNode, targetNode); 229 | addToNeighborhood(targetNode, closestNode); 230 | } 231 | } 232 | 233 | findNextNode(currNode: Node, targetNode: Node, level: number): Node | null { 234 | let nextNode = null; 235 | let maxSimilarity = -Infinity; 236 | 237 | // traverse along the neigboring nodes of curNode 238 | for (const neighborId of currNode.neighbors[level]) { 239 | // lone node is the closest next node in the level 240 | if (neighborId === -1) break; 241 | 242 | // pick the neighbor node and check if it's closer 243 | const neighborNode = this.nodes.get(neighborId)!; 244 | const neighborSimilarity = this.similarityFunction( 245 | targetNode.embedding, 246 | neighborNode.embedding 247 | ); 248 | // if so, make it the nextNode 249 | if (neighborSimilarity > maxSimilarity) { 250 | maxSimilarity = neighborSimilarity; 251 | nextNode = neighborNode; 252 | } 253 | } 254 | return nextNode; 255 | } 256 | 257 | serialize() { 258 | /* 259 | Sample Serialized 260 | { 261 | "M": 16, 262 | "efConstruction": 200, 263 | "levelMax": 3, 264 | "entryPointId": 1, 265 | "nodes": [ 266 | { 267 | "id": 1, 268 | "level": 2, 269 | "embedding": [0.5, 0.3, 0.8], 270 | "neighbors": [[2, 3], [4, 5], [6, 7]] 271 | }, 272 | { 273 | "id": 2, 274 | "level": 1, 275 | "embedding": [0.2, 0.7, 0.1], 276 | "neighbors": [[1, 3], [8, 9]] 277 | }, 278 | { 279 | "id": 3, 280 | "level": 2, 281 | "embedding": [0.9, 0.4, 0.6], 282 | "neighbors": [[1, 2], [10, 11], [12, 13]] 283 | }, 284 | // ... additional nodes ... 285 | ] 286 | } 287 | */ 288 | const entries = Array.from(this.nodes.entries()); 289 | const nodes = entries.map(([id, node]) => { 290 | return [ 291 | id, 292 | { 293 | id: node.id, 294 | content: node.content, 295 | level: node.level, 296 | embedding: Array.from(node.embedding), 297 | neighbors: node.neighbors.map((level) => Array.from(level)) 298 | } 299 | ]; 300 | }); 301 | 302 | return { 303 | M: this.M, 304 | efConstruction: this.efConstruction, 305 | levelMax: this.levelMax, 306 | entryPointId: this.entryPointId, 307 | node: nodes 308 | }; 309 | } 310 | 311 | static deserialize(data: any): HNSW { 312 | // deserialize json data 313 | const hnsw = new HNSW(data.M, data.efConstruction); 314 | hnsw.levelMax = data.levelMax; 315 | hnsw.entryPointId = data.entryPointId; 316 | hnsw.nodes = new Map( 317 | data.nodes.map(([id, node]: [number, any]) => { 318 | return [ 319 | id, 320 | { 321 | ...node, 322 | embedding: new Float32Array(node.embedding) 323 | } 324 | ]; 325 | }) 326 | ); 327 | return hnsw; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // index exports 2 | export { Embedding, Node } from "./node"; 3 | export { SimilarityMetric } from "./metric"; 4 | export { HNSW, vectorReducer, vectorResult, vectorTransformer } from "./hnsw"; 5 | 6 | // store exports 7 | export { VectorStore } from "./store"; 8 | 9 | // options and errors 10 | export { VectorStoreOptions } from "./options"; 11 | export { 12 | VectorStoreIndexMissing, 13 | VectorStoreIndexPurgeFailed, 14 | VectorStoreUnintialized 15 | } from "./errors"; 16 | -------------------------------------------------------------------------------- /src/metric.ts: -------------------------------------------------------------------------------- 1 | export enum SimilarityMetric { 2 | cosine = "cosine", 3 | euclidean = "euclidean" 4 | } 5 | 6 | import { Embedding } from "./node"; 7 | 8 | function dotProduct(a: Embedding, b: Embedding): number { 9 | let result = 0; 10 | for (let i = 0; i < a.length; i++) { 11 | result += a[i] * b[i]; 12 | } 13 | return result; 14 | } 15 | 16 | function euclideanDistance(a: Embedding, b: Embedding): number { 17 | let result = 0.0; 18 | for (let i = 0; i < a.length; i++) { 19 | result += (a[i] - b[i]) ** 2; 20 | } 21 | result = Math.sqrt(result); 22 | return result; 23 | } 24 | 25 | export function cosineSimilarity(a: Embedding, b: Embedding): number { 26 | return ( 27 | dotProduct(a, b) / 28 | (Math.sqrt(dotProduct(a, a)) * Math.sqrt(dotProduct(b, b))) 29 | ); 30 | } 31 | 32 | export function euclideanSimilarity(a: Embedding, b: Embedding): number { 33 | return 1 / (1 + euclideanDistance(a, b)); 34 | } 35 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | export type Content = string; 2 | export type Embedding = number[] | Float32Array; 3 | export class Node { 4 | id: number; 5 | content: Content; 6 | level: number; 7 | embedding: Embedding; 8 | neighbors: number[][]; 9 | 10 | constructor( 11 | id: number, 12 | level: number, 13 | embedding: Embedding, 14 | maxNeighbors: number, 15 | content: Content 16 | ) { 17 | this.id = id; 18 | this.level = level; 19 | this.embedding = embedding; 20 | this.content = content; 21 | 22 | // fill neighbors arrays with sentinel values 23 | this.neighbors = Array.from( 24 | { 25 | length: level + 1 26 | }, 27 | () => new Array(maxNeighbors).fill(-1) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export type VectorStoreOptions = { 2 | collectionName: string; 3 | M?: number; 4 | efConstruction?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { HNSW } from "./hnsw"; 2 | import { openDB, deleteDB, DBSchema, IDBPDatabase } from "idb"; 3 | import { 4 | VectorStoreIndexPurgeFailed, 5 | VectorStoreRecreationFailed, 6 | VectorStoreUnintialized 7 | } from "./errors"; 8 | import { VectorStoreOptions } from "./options"; 9 | import { Node } from "./node"; 10 | import { SimilarityMetric } from "./metric"; 11 | interface IndexSchema extends DBSchema { 12 | meta: { 13 | key: string; 14 | value: any; 15 | }; 16 | index: { 17 | key: string; 18 | value: any; 19 | }; 20 | } 21 | 22 | export class VectorStore extends HNSW { 23 | collectionName: string; 24 | collection: IDBPDatabase | null = null; 25 | 26 | private constructor( 27 | M: number, 28 | efConstruction: number, 29 | collectionName: string 30 | ) { 31 | super(M, efConstruction); 32 | this.collectionName = collectionName; 33 | } 34 | 35 | static async create({ 36 | collectionName, 37 | M = 16, 38 | efConstruction = 200 39 | }: VectorStoreOptions): Promise { 40 | if (await VectorStore.exists(collectionName)) { 41 | await deleteDB(collectionName); 42 | } 43 | const instance = new VectorStore(M, efConstruction, collectionName); 44 | await instance.init(); 45 | return instance; 46 | } 47 | 48 | // attempt to recreate the index from collection 49 | static async recreate({ 50 | collectionName 51 | }: { 52 | collectionName: string; 53 | }): Promise { 54 | if (!(await VectorStore.exists(collectionName))) 55 | return this.create({ collectionName }); 56 | 57 | const { M, efConstruction } = await VectorStore.getMeta(collectionName); 58 | const instance = new VectorStore(M, efConstruction, collectionName); 59 | await instance.loadIndex(); 60 | return instance; 61 | } 62 | 63 | // check if the collection exists 64 | static async exists(collectionName: string): Promise { 65 | return (await indexedDB.databases()) 66 | .map((db) => db.name) 67 | .includes(collectionName); 68 | } 69 | 70 | private async init() { 71 | this.collection = await openDB(this.collectionName, 1, { 72 | upgrade(collection: IDBPDatabase) { 73 | collection.createObjectStore("meta"); 74 | collection.createObjectStore("index"); 75 | } 76 | }); 77 | } 78 | 79 | // get metadata about any collection 80 | static async getMeta(collectionName: string) { 81 | const collection = await openDB(collectionName, 1); 82 | const efConstruction = await collection.get("meta", "efConstruction"); 83 | const M = await collection.get("meta", "neighbors"); 84 | collection.close(); 85 | return { M, efConstruction, collectionName }; 86 | } 87 | 88 | // save meta info onto meta object store 89 | private async saveMeta() { 90 | if (!this.collection) throw VectorStoreUnintialized; 91 | await this.collection?.put("meta", this.d, "dimension"); 92 | await this.collection?.put( 93 | "meta", 94 | this.collectionName, 95 | "collectionName" 96 | ); 97 | await this.collection?.put("meta", this.M, "neighbors"); 98 | await this.collection?.put("meta", this.entryPointId, "entryPointId"); 99 | await this.collection?.put( 100 | "meta", 101 | this.efConstruction, 102 | "efConstruction" 103 | ); 104 | await this.collection?.put("meta", this.probs, "probs"); 105 | await this.collection?.put("meta", this.levelMax, "levelMax"); 106 | await this.collection?.put( 107 | "meta", 108 | this.metric === SimilarityMetric.cosine ? "cosine" : "euclidean", 109 | "levelMax" 110 | ); 111 | } 112 | 113 | // load meta info from meta object store 114 | private async loadMeta() { 115 | if (!this.collection) throw VectorStoreUnintialized; 116 | this.d = await this.collection?.get("meta", "dimension"); 117 | this.collectionName = await this.collection?.get( 118 | "meta", 119 | "collectionName" 120 | ); 121 | this.M = await this.collection?.get("meta", "neighbors"); 122 | this.entryPointId = await this.collection?.get("meta", "entryPointId"); 123 | this.efConstruction = await this.collection?.get( 124 | "meta", 125 | "efConstruction" 126 | ); 127 | this.probs = await this.collection?.get("meta", "probs"); 128 | this.levelMax = await this.collection?.get("meta", "levelMax"); 129 | this.metric = 130 | (await this.collection?.get("meta", "levelMax")) === "cosine" 131 | ? SimilarityMetric.cosine 132 | : SimilarityMetric.euclidean; 133 | } 134 | 135 | // save index into index object store 136 | async saveIndex() { 137 | if (!this.collection) throw VectorStoreUnintialized; 138 | // delete index if it exists already 139 | if (await VectorStore.exists(this.collectionName)) 140 | await this.deleteIndex(); 141 | // populate the index 142 | const nodes = this.nodes; 143 | nodes.forEach(async (node: Node, key: number) => { 144 | await this.collection?.put("index", node, String(key)); 145 | }); 146 | // populate the meta 147 | await this.saveMeta(); 148 | } 149 | 150 | // load index into index object store 151 | private async loadIndex() { 152 | if (!this.collection) throw VectorStoreUnintialized; 153 | try { 154 | // hold the nodes loaded in from indexedDB 155 | const nodes: Map = new Map(); 156 | let store = this.collection.transaction("index").store; 157 | let cursor = await store.openCursor(); 158 | // traverse the cursor 159 | while (true) { 160 | const id = Number(cursor?.key); 161 | const node = cursor?.value; 162 | 163 | nodes.set(id, node); 164 | 165 | cursor = await cursor!.continue(); 166 | if (!cursor) break; 167 | } 168 | // map the nodes 169 | this.nodes = nodes; 170 | // load the meta data 171 | await this.loadMeta(); 172 | } catch (error) { 173 | throw VectorStoreRecreationFailed; 174 | } 175 | } 176 | 177 | // delete index if it exists and populate it with empty index 178 | async deleteIndex() { 179 | if (!this.collection) return VectorStoreUnintialized; 180 | 181 | try { 182 | await deleteDB(this.collectionName); 183 | this.init(); 184 | } catch (error) { 185 | throw VectorStoreIndexPurgeFailed; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /test/hnsw.test.ts: -------------------------------------------------------------------------------- 1 | import { HNSW } from "../src"; 2 | import { SimilarityMetric } from "../src/metric"; 3 | import { describe, expect } from "vitest"; 4 | 5 | describe("HNSW Test with Cosine Metric", (test) => { 6 | test("should return correct nearest neighbors", async () => { 7 | // Simple example with cosine metric 8 | const hnsw = new HNSW(16, 200, 5, SimilarityMetric.cosine); 9 | 10 | // Make some data 11 | const data = [ 12 | { id: 1, content: "foo", embedding: [1, 2, 3, 4, 5] }, 13 | { id: 2, content: "bar", embedding: [2, 3, 4, 5, 6] }, 14 | { id: 3, content: "sho", embedding: [3, 4, 5, 6, 7] }, 15 | { id: 4, content: "que", embedding: [4, 5, 6, 7, 8] }, 16 | { id: 5, content: "wee", embedding: [5, 6, 7, 8, 9] } 17 | ]; 18 | 19 | // Build the index 20 | await hnsw.buildIndex(data); 21 | 22 | // Search for nearest neighbors 23 | const results = hnsw.query([6, 7, 8, 9, 10], 2); 24 | 25 | expect(results).toEqual([ 26 | { 27 | id: 1, 28 | content: "foo", 29 | embedding: [1, 2, 3, 4, 5], 30 | score: 0.9649505047327671 31 | }, 32 | { 33 | id: 2, 34 | content: "bar", 35 | embedding: [2, 3, 4, 5, 6], 36 | score: 0.9864400504156211 37 | } 38 | ]); 39 | }); 40 | }); 41 | 42 | describe("HNSW Test with Euclidean Metric", (test) => { 43 | test("should return correct nearest neighbors with euclidean distance", async () => { 44 | // Simple example with euclidean distance 45 | const hnsw = new HNSW(16, 200, 5, SimilarityMetric.euclidean); 46 | 47 | // Make some data 48 | const data = [ 49 | { id: 1, content: "foo", embedding: [1, 2, 3, 4, 5] }, 50 | { id: 2, content: "bar", embedding: [2, 3, 4, 5, 6] }, 51 | { id: 3, content: "sho", embedding: [3, 4, 5, 6, 7] }, 52 | { id: 4, content: "que", embedding: [4, 5, 6, 7, 8] }, 53 | { id: 5, content: "wee", embedding: [5, 6, 7, 8, 9] } 54 | ]; 55 | 56 | // Build the index 57 | await hnsw.buildIndex(data); 58 | 59 | // Search for nearest neighbors 60 | const results = hnsw.query([6, 7, 8, 9, 10], 2); 61 | 62 | expect(results).toEqual([ 63 | { 64 | id: 1, 65 | content: "foo", 66 | embedding: [1, 2, 3, 4, 5], 67 | score: 0.08209951522176571 68 | }, 69 | { 70 | id: 2, 71 | content: "bar", 72 | embedding: [2, 3, 4, 5, 6], 73 | score: 0.10056040392403998 74 | } 75 | ]); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 108 | "noEmit": true /* Disable error reporting for missing .d.ts files. */ 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["cjs", "esm"], // Build for commonJS and ESmodules 6 | dts: true, // Generate declaration file (.d.ts) 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true 10 | }); 11 | --------------------------------------------------------------------------------