├── .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 |
--------------------------------------------------------------------------------