Skip to content

webeach/collection

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

52 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

collection

npm version build npm downloads license bundle size

πŸ‡ΊπŸ‡Έ English | πŸ‡·πŸ‡Ί Русский

Managed collection of items with hooks, events, and strict type safety.


πŸ’Ž Features

  • Strongly-typed collection with a configurable primary key
  • Lifecycle hooks (insert, patch, remove, clear) with before/after stages
  • Event-driven updates via onUpdate callback and addEventListener
  • Full support for string, number, and bigint primary keys
  • Zero runtime dependencies

πŸ“¦ Installation

npm install @webeach/collection
pnpm add @webeach/collection
yarn add @webeach/collection

Browser via CDN

No build step needed β€” load directly in the browser via unpkg:

<script type="module">
  import { Collection } from 'https://unpkg.com/@webeach/collection';

  const users = new Collection({ primaryKey: 'id' });
  users.appendItem({ id: 1, name: 'Alice' });
</script>

πŸš€ Quick Start

Adding items

import { Collection } from '@webeach/collection';

const users = new Collection({
  primaryKey: 'id',
});

users.appendItem({ id: 1, firstName: 'Ivan', lastName: 'Petrov' });
users.appendItem({ id: 2, firstName: 'Jason', lastName: 'Statham' });

console.log(users.numItems); // 2
console.log(users.getItem(2)?.firstName); // 'Jason'

Replacing an item

import { Collection } from '@webeach/collection';

const products = new Collection({ primaryKey: 'sku' });

products.appendItem({ sku: 'A001', name: 'Laptop' });
products.replaceItem('A001', { sku: 'A001', name: 'Laptop Pro' });

console.log(products.getItem('A001')?.name); // 'Laptop Pro'

Bulk replacing items with setItems

import { Collection } from '@webeach/collection';

const tasks = new Collection({
  primaryKey: 'id',
  initialItems: [
    { id: 1, title: 'Task 1' },
    { id: 2, title: 'Task 2' },
  ],
});

tasks.setItems([
  { id: 3, title: 'New Task 3' },
  { id: 4, title: 'New Task 4' },
]);

console.log(tasks.numItems); // 2
console.log(tasks.getItem(3)?.title); // 'New Task 3'

Listening for updates

import { Collection } from '@webeach/collection';

const list = new Collection({ primaryKey: 'id' });

list.onUpdate = (event) => {
  console.log('Items updated:', event.detail);
};

// Or via addEventListener
list.addEventListener('update', (event) => {
  console.log('Items updated:', event.detail);
});

list.appendItem({ id: 1, name: 'Alice' });

Using lifecycle hooks

import {
  Collection,
  $CollectionHookDispatcherSymbol,
} from '@webeach/collection';

const users = new Collection({ primaryKey: 'id' });

// Block insertion of items with even ids
const { unregister } = users[$CollectionHookDispatcherSymbol].register(
  'insert:before',
  ({ item }) => {
    if (item.id % 2 === 0) {
      return false; // cancel insertion
    }
  },
);

users.appendItem({ id: 1, name: 'Alice' }); // succeeds
users.appendItem({ id: 2, name: 'Bob' }); // blocked

console.log(users.numItems); // 1

unregister();

πŸ› οΈ API

Collection

CollectionUpdateEvent


🧩 TypeScript

The collection is fully generic and infers types based on the primary key and item shape.

import { Collection } from '@webeach/collection';

interface User {
  id: number;
  name: string;
  role: 'admin' | 'user';
}

const users = new Collection<'id', number, User>({
  primaryKey: 'id',
});

users.appendItem({ id: 1, name: 'Alice', role: 'admin' });

const user = users.getItem(1);
// user: CollectionItem<'id', number, User> | null

πŸ“– Real-world Examples

Tracking a list in React

import { FC, useEffect, useRef, useState } from 'react';
import { Collection } from '@webeach/collection';

interface Task {
  id: number;
  title: string;
  done: boolean;
}

export const TaskList: FC = () => {
  const collectionRef = useRef(
    new Collection<'id', number, Task>({ primaryKey: 'id' }),
  );
  const [tasks, setTasks] = useState<Task[]>([]);

  useEffect(() => {
    const collection = collectionRef.current;

    collection.onUpdate = (event) => {
      setTasks([...event.detail] as Task[]);
    };

    collection.appendItem({ id: 1, title: 'Buy groceries', done: false });
    collection.appendItem({ id: 2, title: 'Write tests', done: false });
  }, []);

  const toggle = (id: number) => {
    const item = collectionRef.current.getItem(id);

    if (item) {
      collectionRef.current.patchItem(id, { done: !item.done });
    }
  };

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id} onClick={() => toggle(task.id)}>
          {task.done ? 'βœ“' : 'β—‹'} {task.title}
        </li>
      ))}
    </ul>
  );
};

Enforcing a max size via hook

import {
  Collection,
  $CollectionHookDispatcherSymbol,
} from '@webeach/collection';

function createBoundedCollection<T extends { id: number }>(maxSize: number) {
  const collection = new Collection<'id', number, T>({ primaryKey: 'id' });

  collection[$CollectionHookDispatcherSymbol].register('insert:before', () => {
    if (collection.numItems >= maxSize) {
      return false;
    }
  });

  return collection;
}

const limited = createBoundedCollection(3);

limited.appendItem({ id: 1 }); // ok
limited.appendItem({ id: 2 }); // ok
limited.appendItem({ id: 3 }); // ok
limited.appendItem({ id: 4 }); // blocked β€” limit reached

console.log(limited.numItems); // 3

πŸ‘¨β€πŸ’» Author

Development and support: Ruslan Martynov

If you have suggestions or found a bug, feel free to open an issue or submit a pull request.


πŸ“„ License

This package is distributed under the MIT License.

Contributors