Skip to content

Commit 86106ba

Browse files
committed
feat: add object pool utility
1 parent c6cdd5c commit 86106ba

9 files changed

Lines changed: 183 additions & 3 deletions

File tree

PROJECT_DESCRIPTION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ npm run build
3838
| 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` |
3939
| 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` |
4040
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
41-
| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts` |
41+
| 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` |
4242
| Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
4343
| Data transforms & diffing | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
4444
| Graph traversal | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ CDN usage:
2828
| 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` |
2929
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
3030
| 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` |
31-
| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts` |
31+
| 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` |
3232
| Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
3333
| Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` |
3434
| Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` |

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
- Gameplay systems & utilities:
4747
- [ ] Fixed-timestep game loop utility with interpolation helpers
4848
- [ ] Delta-time manager for frame-independent timing
49-
- [ ] Object pool helper for reusable entities
49+
- [x] Object pool helper for reusable entities
5050
- [x] Weighted random selector (alias method)
5151
- [ ] Fisher–Yates shuffle implementation
5252
- [ ] Bresenham line / raster traversal helpers

docs/index.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export const examples: {
9090
readonly clearRequestDedup: 'examples/requestDedup.ts';
9191
readonly calculateVirtualRange: 'examples/virtualScroll.ts';
9292
readonly createWeightedAliasSampler: 'examples/weightedAlias.ts';
93+
readonly createObjectPool: 'examples/objectPool.ts';
9394
};
9495
readonly ai: {
9596
readonly seek: 'examples/steering.ts';
@@ -819,6 +820,37 @@ export function createWeightedAliasSampler<T>(
819820
entries: ReadonlyArray<WeightedAliasEntry<T>>
820821
): WeightedAliasSampler<T>;
821822

823+
/**
824+
* Object pool options.
825+
* Use for: configuring factories, reset functions, and pool sizing.
826+
* Import: util/objectPool.ts
827+
*/
828+
export interface ObjectPoolOptions<T> {
829+
factory: () => T;
830+
reset?: (item: T) => void;
831+
initialSize?: number;
832+
maxSize?: number;
833+
}
834+
835+
/**
836+
* Object pool API exposing acquire/release.
837+
* Import: util/objectPool.ts
838+
*/
839+
export interface ObjectPool<T> {
840+
acquire(): T;
841+
release(item: T): void;
842+
available(): number;
843+
size(): number;
844+
}
845+
846+
/**
847+
* Creates an object pool for reusing allocations.
848+
* Use for: performance sensitive systems and resource recycling.
849+
* Performance: O(1) acquire/release with optional reset handling.
850+
* Import: util/objectPool.ts
851+
*/
852+
export function createObjectPool<T>(options: ObjectPoolOptions<T>): ObjectPool<T>;
853+
822854
/**
823855
* Least recently used cache.
824856
* Use for: memoizing responses, data loaders, pagination caches.

examples/objectPool.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createObjectPool } from '../src/index.js';
2+
3+
interface Particle {
4+
id: number;
5+
active: boolean;
6+
}
7+
8+
let nextId = 1;
9+
const pool = createObjectPool<Particle>({
10+
factory: () => ({ id: nextId += 1, active: true }),
11+
reset: (particle) => {
12+
particle.active = true;
13+
},
14+
initialSize: 2,
15+
maxSize: 5,
16+
});
17+
18+
const particle = pool.acquire();
19+
console.log('Acquired particle:', particle);
20+
particle.active = false;
21+
pool.release(particle);
22+
console.log('Available after release:', pool.available());

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const examples = {
8686
clearRequestDedup: 'examples/requestDedup.ts',
8787
calculateVirtualRange: 'examples/virtualScroll.ts',
8888
createWeightedAliasSampler: 'examples/weightedAlias.ts',
89+
createObjectPool: 'examples/objectPool.ts',
8990
},
9091
ai: {
9192
seek: 'examples/steering.ts',
@@ -415,6 +416,13 @@ export { calculateVirtualRange } from './util/virtualScroll.js';
415416
*/
416417
export { createWeightedAliasSampler } from './util/weightedAlias.js';
417418

419+
/**
420+
* Object pool helper for reusing allocations.
421+
*
422+
* Example file: examples/objectPool.ts
423+
*/
424+
export { createObjectPool } from './util/objectPool.js';
425+
418426
/**
419427
* Virtual scroll type exports to help define rendering contracts.
420428
*/

src/util/objectPool.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
export interface ObjectPoolOptions<T> {
2+
factory: () => T;
3+
reset?: (item: T) => void;
4+
initialSize?: number;
5+
maxSize?: number;
6+
}
7+
8+
export interface ObjectPool<T> {
9+
acquire(): T;
10+
release(item: T): void;
11+
available(): number;
12+
size(): number;
13+
}
14+
15+
/**
16+
* Creates an object pool for reusing allocations.
17+
* Useful for: performance-critical loops, game entities, temporary buffers.
18+
*/
19+
export function createObjectPool<T>({
20+
factory,
21+
reset,
22+
initialSize = 0,
23+
maxSize = Number.POSITIVE_INFINITY,
24+
}: ObjectPoolOptions<T>): ObjectPool<T> {
25+
if (typeof factory !== 'function') {
26+
throw new Error('factory must be a function.');
27+
}
28+
if (initialSize < 0) {
29+
throw new Error('initialSize must be >= 0.');
30+
}
31+
if (maxSize < initialSize) {
32+
throw new Error('maxSize must be >= initialSize.');
33+
}
34+
35+
const freeList: T[] = [];
36+
for (let i = 0; i < initialSize; i += 1) {
37+
freeList.push(factory());
38+
}
39+
40+
let totalCreated = initialSize;
41+
42+
function acquire(): T {
43+
if (freeList.length > 0) {
44+
return freeList.pop()!;
45+
}
46+
if (totalCreated >= maxSize) {
47+
throw new Error('Object pool depleted and reached maxSize.');
48+
}
49+
totalCreated += 1;
50+
return factory();
51+
}
52+
53+
function release(item: T): void {
54+
if (reset) {
55+
reset(item);
56+
}
57+
freeList.push(item);
58+
}
59+
60+
function available(): number {
61+
return freeList.length;
62+
}
63+
64+
function size(): number {
65+
return totalCreated;
66+
}
67+
68+
return { acquire, release, available, size };
69+
}

tests/index.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ describe('package entry point', () => {
2121
expect(examples.procedural.generateAldousBroderMaze).toBe('examples/mazeAldous.ts');
2222
expect(examples.procedural.generateRecursiveDivisionMaze).toBe('examples/mazeDivision.ts');
2323
expect(examples.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts');
24+
expect(examples.performance.createObjectPool).toBe('examples/objectPool.ts');
25+
expect(examples.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts');
2426
expect(examples.search.Trie).toBe('examples/search.ts');
2527
expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts');
2628
});
@@ -72,5 +74,17 @@ describe('package entry point', () => {
7274
| 'generateAldousBroderMaze'
7375
| 'generateRecursiveDivisionMaze'
7476
>();
77+
78+
expectTypeOf<ExampleName<'performance'>>().toEqualTypeOf<
79+
| 'debounce'
80+
| 'throttle'
81+
| 'LRUCache'
82+
| 'memoize'
83+
| 'deduplicateRequest'
84+
| 'clearRequestDedup'
85+
| 'calculateVirtualRange'
86+
| 'createWeightedAliasSampler'
87+
| 'createObjectPool'
88+
>();
7589
});
7690
});

tests/objectPool.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { createObjectPool } from '../src/index.js';
4+
5+
describe('createObjectPool', () => {
6+
it('reuses objects and respects reset', () => {
7+
let created = 0;
8+
const pool = createObjectPool({
9+
factory: () => ({ used: false, id: created += 1 }),
10+
reset: (item) => {
11+
item.used = false;
12+
},
13+
initialSize: 1,
14+
});
15+
16+
const first = pool.acquire();
17+
first.used = true;
18+
pool.release(first);
19+
20+
const second = pool.acquire();
21+
expect(second).toBe(first);
22+
expect(second.used).toBe(false);
23+
});
24+
25+
it('throws when pool depleted beyond maxSize', () => {
26+
const pool = createObjectPool({
27+
factory: () => ({}),
28+
initialSize: 0,
29+
maxSize: 1,
30+
});
31+
32+
pool.acquire();
33+
expect(() => pool.acquire()).toThrow();
34+
});
35+
});

0 commit comments

Comments
 (0)