init commit

This commit is contained in:
Eugene 2025-12-02 22:22:29 +03:00
commit 1b863fccfe
12 changed files with 4226 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2025 yazmeyaa
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

81
README.md Normal file
View File

@ -0,0 +1,81 @@
### TS SparseSet
[![NPM version][npm-image]][npm-url]
[![Downloads][downloads-image]][downloads-url]
**TS SparseSet** — это высокопроизводительная TypeScript структура данных, реализующая *sparse set*, позволяющая хранить и быстро управлять ID-ориентированными коллекциями и компонентами ECS.
#### Features
* **O(1) Operations:** `add`, `remove`, `get`, and `has` с плотным хранением элементов.
* **Dense Storage:** Эффективная плотная память для максимальной производительности и locality.
* **Swap-Remove:** При удалении элементов массив остаётся компактным.
* **Reusable Structure:** Может использоваться для ECS-компонентов, сущностей, пулов объектов или других ID-ориентированных коллекций.
* **Optional Object Pool Integration:** Лёгкая интеграция с пулами объектов для минимизации аллокаций.
* **TypeScript Native:** Полная типизация и совместимость с современным TS.
#### Installation
```bash
npm install ts-sparse-set
```
#### Usage Example
```typescript
import { SparseSet } from 'ts-sparse-set';
// Создаём новый SparseSet для чисел
const set = new SparseSet<number>();
// Добавляем элементы
set.add(10, 42);
set.add(20, 100);
// Проверяем наличие
console.log(set.has(10)); // true
console.log(set.has(5)); // false
// Получаем значение
console.log(set.get(20)); // 100
// Перезаписываем значение
set.add(10, 50);
console.log(set.get(10)); // 50
// Удаляем элемент
set.remove(20);
console.log(set.has(20)); // false
// Добавляем несколько элементов и проверяем плотность
set.add(30, 300);
set.add(40, 400);
set.remove(10);
console.log(set.get(30)); // 300
console.log(set.get(40)); // 400
```
#### API Documentation
* **SparseSet Class**
* **Constructor:** `new SparseSet<V>()`
* **Methods:**
* `add(id: number, value: V): V` — добавляет новый элемент или обновляет существующий.
* `get(id: number): V | null` — возвращает элемент по ID или `null`.
* `has(id: number): boolean` — проверяет наличие элемента.
* `remove(id: number): void` — удаляет элемент и поддерживает плотность dense массива.
#### Note
SparseSet идеально подходит для ECS-сценариев, где сущности и компоненты идентифицируются числами. Эта структура не является обычным Map/Set и оптимизирована под плотные массивы с минимальной стоимостью операций.
---
Feel free to contribute, report issues, or suggest improvements on [GitHub](https://github.com/yazmeyaa/ts-sparse-set).
[npm-image]: https://img.shields.io/npm/v/ts-sparse-set.svg?style=flat-square
[npm-url]: https://npmjs.org/package/ts-sparse-set
[downloads-image]: https://img.shields.io/npm/dm/ts-sparse-set.svg?style=flat-square
[downloads-url]: https://npmjs.org/package/ts-sparse-set

7
jest.config.js Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: "node",
transform: {
"^.+\.tsx?$": ["ts-jest",{}],
},
};

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "ts-sparse-set",
"private": false,
"version": "0.0.0",
"license": "MIT",
"type": "module",
"author": "yazmeyaa",
"description": "A high-performance, TypeScript-native SparseSet implementation for ECS and other ID-based collections. Provides O(1) add, remove, get, and has operations with dense storage and optional object pooling for components.",
"homepage": "https://github.com/yazmeyaa/ts-sparse-set#readme",
"bugs": {
"url": "https://github.com/yazmeyaa/ts-sparse-set/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/yazmeyaa/ts-sparse-set.git"
},
"main": "./dist/index.umd.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"LICENSE",
"README.md"
],
"sideEffects": false,
"engines": {
"node": ">=16"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.umd.cjs"
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
"keywords": [],
"scripts": {
"build": "tsc && vite build",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"format": "prettier --write ./src/**/*.ts",
"test": "jest"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"jest": "^30.2.0",
"prettier": "^3.7.3",
"rollup-plugin-typescript-paths": "^1.5.0",
"ts-jest": "^29.4.6",
"tslib": "^2.8.1",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^5.1.4"
},
"dependencies": {}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export { SparseSet } from "./sparse_set";

92
src/sparse_set.test.ts Normal file
View File

@ -0,0 +1,92 @@
import { SparseSet } from "./sparse_set";
describe("SparseSet", () => {
let set: SparseSet<number>;
beforeEach(() => {
set = new SparseSet<number>();
});
test("add and get component", () => {
set.add(5, 42);
expect(set.get(5)).toBe(42);
expect(set.has(5)).toBe(true);
});
test("has returns false for missing element", () => {
expect(set.has(1)).toBe(false);
expect(set.get(1)).toBeNull();
});
test("overwrite existing component", () => {
set.add(3, 10);
set.add(3, 20);
expect(set.get(3)).toBe(20);
expect(set.has(3)).toBe(true);
});
test("remove element", () => {
set.add(1, 100);
set.add(2, 200);
set.add(3, 300);
set.remove(2);
expect(set.has(2)).toBe(false);
expect(set.get(2)).toBeNull();
expect(set.has(1)).toBe(true);
expect(set.has(3)).toBe(true);
expect(set.get(1)).toBe(100);
expect(set.get(3)).toBe(300);
});
test("remove last element", () => {
set.add(7, 77);
set.remove(7);
expect(set.has(7)).toBe(false);
expect(set.get(7)).toBeNull();
});
test("swap-remove maintains correct mapping", () => {
set.add(10, 100);
set.add(20, 200);
set.add(30, 300);
set.remove(20);
expect(set.has(10)).toBe(true);
expect(set.get(10)).toBe(100);
expect(set.has(30)).toBe(true);
expect(set.get(30)).toBe(300);
const denseIds = [10, 30].map((id) => set.get(id));
expect(denseIds).toEqual([100, 300]);
});
test("sparse dynamically grows", () => {
set.add(50, 500);
expect(set.has(50)).toBe(true);
expect(set.get(50)).toBe(500);
});
test("removing non-existent element does not crash", () => {
set.add(1, 10);
expect(() => set.remove(99)).not.toThrow();
});
test("adding multiple elements and removing all", () => {
for (let i = 0; i < 5; i++) {
set.add(i, i * 10);
}
for (let i = 0; i < 5; i++) {
expect(set.has(i)).toBe(true);
}
for (let i = 0; i < 5; i++) {
set.remove(i);
expect(set.has(i)).toBe(false);
expect(set.get(i)).toBeNull();
}
});
});

74
src/sparse_set.ts Normal file
View File

@ -0,0 +1,74 @@
export class SparseSet<V> {
private sparse: number[] = [];
private dense: number[] = [];
private data: V[] = [];
public has(id: number): boolean {
const index = this.sparse[id];
return (
index !== undefined &&
index < this.dense.length &&
this.dense[index] === id
);
}
public get(id: number): V | null {
const index = this.sparse[id];
if (
index === undefined ||
index >= this.dense.length ||
this.dense[index] !== id
) {
return null;
}
return this.data[index];
}
public add(id: number, value: V): V {
const existing = this.sparse[id];
if (
existing !== undefined &&
existing < this.dense.length &&
this.dense[existing] === id
) {
this.data[existing] = value;
return value;
}
const index = this.dense.length;
this.dense[index] = id;
this.data[index] = value;
if (this.sparse.length <= id) {
this.sparse.length = id + 1;
}
this.sparse[id] = index;
return value;
}
public remove(id: number): void {
const index = this.sparse[id];
if (
index === undefined ||
index >= this.dense.length ||
this.dense[index] !== id
) {
return;
}
const lastIndex = this.dense.length - 1;
const lastId = this.dense[lastIndex];
if (index !== lastIndex) {
this.dense[index] = lastId;
this.data[index] = this.data[lastIndex];
this.sparse[lastId] = index;
}
this.dense.pop();
this.data.pop();
delete this.sparse[id];
}
}

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

16
vite.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { defineConfig } from "vite";
import path from "path";
import dts from 'vite-plugin-dts';
const libConfig = defineConfig({
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
fileName: "index",
name: 'bitmap-index'
},
},
plugins: [dts({exclude: "**/*.test.ts"})],
});
export default libConfig

3832
yarn.lock Normal file

File diff suppressed because it is too large Load Diff