Skip to content

pool()

A lazy-instantiation cache with factory function. Items are created on first access and reused thereafter.

When to Use

Use pool when you need keyed singletons — the same key always returns the same instance:

  • Services by endpoint: API clients, database connections
  • Resource management: WebSocket connections, workers
  • Cached objects: Expensive computations, parsed configurations
  • Keyed singletons: Loggers per module, validators per schema
ts
// ✅ Good: Same key = same instance
const loggerPool = pool((module: string) => createLogger(module));
loggerPool("auth"); // Creates logger
loggerPool("auth"); // Returns same logger

// ❌ Not for: Per-call instances (just use a function)
const createForm = (id: string) => new Form(id);

Signature

ts
function pool<TValue, TKey = unknown>(
  createItem: (key: TKey) => TValue,
  options?: PoolOptions<TKey, TValue> | readonly [TKey, TValue][]
): Pool<TKey, TValue>

Options

initial

Pre-populate the pool with items.

  • Type: readonly [TKey, TValue][]
  • Required: No

keyOf

Hash function for O(1) lookups with complex keys.

  • Type: (key: TKey) => string | number
  • Required: No
ts
// Object keys - hash by id
const userPool = pool(createUser, {
  keyOf: (user) => user.id,
});

// Array keys - hash by JSON
const coordPool = pool(createTile, {
  keyOf: JSON.stringify,
});

equality

Custom equality function for key comparison. Uses O(n) linear scan.

  • Type: Equality<TKey> ("strict" | "shallow" | "deep" | (a, b) => boolean)
  • Required: No
  • Default: Strict equality (===)

TIP

Prefer keyOf over equality for better performance.

autoDispose

Automatically call dispose() on items when removed.

  • Type: boolean | AutoDisposeOptions
  • Required: No
  • Default: false
ts
const formPool = pool(createForm, { autoDispose: true });

formPool.get("checkout"); // Creates form
formPool.delete("checkout"); // Calls form.dispose()

Return Value

Returns a Pool<TKey, TValue> that is callable and has these methods:

MethodDescription
(key)Callable - same as get(key)
get(key)Get or create item by key
has(key)Check if key exists (doesn't create)
set(key, value)Explicitly set an item
delete(key)Remove item (calls dispose if enabled)
clear()Remove all items (calls dispose if enabled)
tap(key, fn)Call fn if key exists (doesn't create)
size()Number of items
keys()Iterator over keys
values()Iterator over values
entries()Iterator over [key, value] pairs

Examples

API Clients by Endpoint

ts
import { pool } from "storion";

// One client per base URL
const apiClientPool = pool((baseUrl: string) => ({
  baseUrl,
  fetch: async (path: string, options?: RequestInit) => {
    const res = await fetch(`${baseUrl}${path}`, options);
    return res.json();
  },
}));

// Usage
const github = apiClientPool("https://api.github.com");
const gitlab = apiClientPool("https://gitlab.com/api/v4");
const github2 = apiClientPool("https://api.github.com"); // Same instance

WebSocket Connections

ts
const wsPool = pool(
  (channel: string) => {
    const socket = new WebSocket(`wss://api.example.com/${channel}`);
    return {
      socket,
      send: (data: unknown) => socket.send(JSON.stringify(data)),
      dispose() {
        socket.close();
      },
    };
  },
  { autoDispose: true }
);

// Connect to channels
wsPool("notifications").socket.onmessage = handleNotification;
wsPool("chat").send({ type: "join" });

// Cleanup
wsPool.delete("chat"); // Closes WebSocket

Loggers by Module

ts
const loggerPool = pool((module: string) => ({
  module,
  log: (msg: string) => console.log(`[${module}] ${msg}`),
  error: (msg: string) => console.error(`[${module}] ${msg}`),
}));

// Different modules, separate loggers
const authLogger = loggerPool("auth");
const dbLogger = loggerPool("database");

authLogger.log("User logged in"); // [auth] User logged in
dbLogger.log("Query executed"); // [database] Query executed

With Complex Keys

ts
type CacheKey = { userId: string; resource: string };

const cachePool = pool(
  (key: CacheKey) => ({
    data: null,
    fetchedAt: null as Date | null,
  }),
  {
    keyOf: (key) => `${key.userId}:${key.resource}`,
  }
);

// Different object, same hash = same item
cachePool.get({ userId: "1", resource: "profile" });
cachePool.get({ userId: "1", resource: "profile" }); // Same item

With Auto-Dispose

ts
const connectionPool = pool(
  (endpoint: string) => ({
    socket: new WebSocket(endpoint),
    dispose() {
      this.socket.close();
    },
  }),
  { autoDispose: true }
);

connectionPool.get("wss://api.example.com");
connectionPool.delete("wss://api.example.com"); // Closes WebSocket

IIFE for Shared Closure

ts
const counterPool = pool(
  (() => {
    // Shared across all items in this pool
    let globalIdCounter = 0;

    return (key: string) => ({
      id: globalIdCounter++,
      key,
      count: 0,
    });
  })()
);

counterPool.get("a").id; // 0
counterPool.get("b").id; // 1
counterPool.get("a").id; // Still 0 (same item)

tap() for Conditional Operations

ts
const formPool = pool(createForm);

// Only save if form exists
formPool.tap("checkout", (form) => {
  form.save();
});

// Chain multiple taps
formPool
  .tap("billing", (form) => form.validate())
  .tap("shipping", (form) => form.validate())
  .tap("payment", (form) => form.submit());

Performance

OperationWithout keyOfWith keyOf
get/has/deleteO(1) or O(n)*O(1)
setO(1) or O(n)*O(1)
clearO(n)O(n)

* O(n) when using custom equality, O(1) with default strict equality.

Released under the MIT License.