diff --git a/PROJECT_DESCRIPTION.md b/PROJECT_DESCRIPTION.md index 4c0e630..b1a29da 100644 --- a/PROJECT_DESCRIPTION.md +++ b/PROJECT_DESCRIPTION.md @@ -38,7 +38,7 @@ npm run build | Grid pathfinding | `astar`, `dijkstra`, `jumpPointSearch`, `computeFlowField`, `buildNavMesh`, `findNavMeshPath`, `manhattanDistance`, `gridFromString` | `pathfinding/astar.ts`, `pathfinding/dijkstra.ts`, `pathfinding/jumpPointSearch.ts`, `pathfinding/flowField.ts`, `pathfinding/navMesh.ts` | `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts` | | Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts` | | Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` | -| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts` | +| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts` | | Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` | | Data transforms & diffing | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` | | Graph traversal | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` | diff --git a/README.md b/README.md index a2df5c9..e26f7ec 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ CDN usage: | Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts` | | Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` | | AI behaviours & crowds | `seek`, `flee`, `arrive`, `pursue`, `wander`, `updateBoids`, `BehaviorTree`, `rvoStep` | `ai/steering.ts`, `ai/boids.ts`, `ai/behaviorTree.ts`, `ai/rvo.ts` | `examples/steering.ts`, `examples/boids.ts`, `examples/rvo.ts` | -| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts` | +| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts` | | Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` | | Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` | | Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` | diff --git a/ROADMAP.md b/ROADMAP.md index 9a86526..5d11b6d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -46,7 +46,7 @@ - Gameplay systems & utilities: - [ ] Fixed-timestep game loop utility with interpolation helpers - [ ] Delta-time manager for frame-independent timing - - [ ] Object pool helper for reusable entities + - [x] Object pool helper for reusable entities - [x] Weighted random selector (alias method) - [ ] Fisher–Yates shuffle implementation - [ ] Bresenham line / raster traversal helpers diff --git a/docs/index.d.ts b/docs/index.d.ts index 1964afa..2238d51 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -90,6 +90,7 @@ export const examples: { readonly clearRequestDedup: 'examples/requestDedup.ts'; readonly calculateVirtualRange: 'examples/virtualScroll.ts'; readonly createWeightedAliasSampler: 'examples/weightedAlias.ts'; + readonly createObjectPool: 'examples/objectPool.ts'; }; readonly ai: { readonly seek: 'examples/steering.ts'; @@ -819,6 +820,37 @@ export function createWeightedAliasSampler( entries: ReadonlyArray> ): WeightedAliasSampler; +/** + * Object pool options. + * Use for: configuring factories, reset functions, and pool sizing. + * Import: util/objectPool.ts + */ +export interface ObjectPoolOptions { + factory: () => T; + reset?: (item: T) => void; + initialSize?: number; + maxSize?: number; +} + +/** + * Object pool API exposing acquire/release. + * Import: util/objectPool.ts + */ +export interface ObjectPool { + acquire(): T; + release(item: T): void; + available(): number; + size(): number; +} + +/** + * Creates an object pool for reusing allocations. + * Use for: performance sensitive systems and resource recycling. + * Performance: O(1) acquire/release with optional reset handling. + * Import: util/objectPool.ts + */ +export function createObjectPool(options: ObjectPoolOptions): ObjectPool; + /** * Least recently used cache. * Use for: memoizing responses, data loaders, pagination caches. diff --git a/examples/objectPool.ts b/examples/objectPool.ts new file mode 100644 index 0000000..c4f9652 --- /dev/null +++ b/examples/objectPool.ts @@ -0,0 +1,22 @@ +import { createObjectPool } from '../src/index.js'; + +interface Particle { + id: number; + active: boolean; +} + +let nextId = 1; +const pool = createObjectPool({ + factory: () => ({ id: nextId += 1, active: true }), + reset: (particle) => { + particle.active = true; + }, + initialSize: 2, + maxSize: 5, +}); + +const particle = pool.acquire(); +console.log('Acquired particle:', particle); +particle.active = false; +pool.release(particle); +console.log('Available after release:', pool.available()); diff --git a/src/index.ts b/src/index.ts index 5c87705..8e72fcc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,7 @@ export const examples = { clearRequestDedup: 'examples/requestDedup.ts', calculateVirtualRange: 'examples/virtualScroll.ts', createWeightedAliasSampler: 'examples/weightedAlias.ts', + createObjectPool: 'examples/objectPool.ts', }, ai: { seek: 'examples/steering.ts', @@ -415,6 +416,13 @@ export { calculateVirtualRange } from './util/virtualScroll.js'; */ export { createWeightedAliasSampler } from './util/weightedAlias.js'; +/** + * Object pool helper for reusing allocations. + * + * Example file: examples/objectPool.ts + */ +export { createObjectPool } from './util/objectPool.js'; + /** * Virtual scroll type exports to help define rendering contracts. */ diff --git a/src/util/objectPool.ts b/src/util/objectPool.ts new file mode 100644 index 0000000..8e6df5a --- /dev/null +++ b/src/util/objectPool.ts @@ -0,0 +1,69 @@ +export interface ObjectPoolOptions { + factory: () => T; + reset?: (item: T) => void; + initialSize?: number; + maxSize?: number; +} + +export interface ObjectPool { + acquire(): T; + release(item: T): void; + available(): number; + size(): number; +} + +/** + * Creates an object pool for reusing allocations. + * Useful for: performance-critical loops, game entities, temporary buffers. + */ +export function createObjectPool({ + factory, + reset, + initialSize = 0, + maxSize = Number.POSITIVE_INFINITY, +}: ObjectPoolOptions): ObjectPool { + if (typeof factory !== 'function') { + throw new Error('factory must be a function.'); + } + if (initialSize < 0) { + throw new Error('initialSize must be >= 0.'); + } + if (maxSize < initialSize) { + throw new Error('maxSize must be >= initialSize.'); + } + + const freeList: T[] = []; + for (let i = 0; i < initialSize; i += 1) { + freeList.push(factory()); + } + + let totalCreated = initialSize; + + function acquire(): T { + if (freeList.length > 0) { + return freeList.pop()!; + } + if (totalCreated >= maxSize) { + throw new Error('Object pool depleted and reached maxSize.'); + } + totalCreated += 1; + return factory(); + } + + function release(item: T): void { + if (reset) { + reset(item); + } + freeList.push(item); + } + + function available(): number { + return freeList.length; + } + + function size(): number { + return totalCreated; + } + + return { acquire, release, available, size }; +} diff --git a/tests/index.test.ts b/tests/index.test.ts index 40b7c44..b8265a4 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -21,6 +21,8 @@ describe('package entry point', () => { expect(examples.procedural.generateAldousBroderMaze).toBe('examples/mazeAldous.ts'); expect(examples.procedural.generateRecursiveDivisionMaze).toBe('examples/mazeDivision.ts'); expect(examples.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts'); + expect(examples.performance.createObjectPool).toBe('examples/objectPool.ts'); + expect(examples.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts'); expect(examples.search.Trie).toBe('examples/search.ts'); expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts'); }); @@ -72,5 +74,17 @@ describe('package entry point', () => { | 'generateAldousBroderMaze' | 'generateRecursiveDivisionMaze' >(); + + expectTypeOf>().toEqualTypeOf< + | 'debounce' + | 'throttle' + | 'LRUCache' + | 'memoize' + | 'deduplicateRequest' + | 'clearRequestDedup' + | 'calculateVirtualRange' + | 'createWeightedAliasSampler' + | 'createObjectPool' + >(); }); }); diff --git a/tests/objectPool.test.ts b/tests/objectPool.test.ts new file mode 100644 index 0000000..026a64a --- /dev/null +++ b/tests/objectPool.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; + +import { createObjectPool } from '../src/index.js'; + +describe('createObjectPool', () => { + it('reuses objects and respects reset', () => { + let created = 0; + const pool = createObjectPool({ + factory: () => ({ used: false, id: created += 1 }), + reset: (item) => { + item.used = false; + }, + initialSize: 1, + }); + + const first = pool.acquire(); + first.used = true; + pool.release(first); + + const second = pool.acquire(); + expect(second).toBe(first); + expect(second.used).toBe(false); + }); + + it('throws when pool depleted beyond maxSize', () => { + const pool = createObjectPool({ + factory: () => ({}), + initialSize: 0, + maxSize: 1, + }); + + pool.acquire(); + expect(() => pool.acquire()).toThrow(); + }); +});