@ebbjs/storage
v0.3.0
Published
Storage components for ebbjs
Readme
@ebbjs/storage
Local-first storage adapters for the ebbjs client. Provides an interface for caching and querying entity data, with in-memory and IndexedDB implementations.
Install
npm install @ebbjs/storage
npm install idb # only for @ebbjs/storage/indexeddbNode >=22.8.0. The root entry is types-only; runtime imports come from a
subpath:
import type { StorageAdapter } from "@ebbjs/storage";
import { createMemoryAdapter } from "@ebbjs/storage/memory";
// also: @ebbjs/storage/indexeddb, @ebbjs/storage/replica, @ebbjs/storage/typesComponents
| Component | Purpose |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ActionLog | Append-ordered log of received actions. append returns the newly-added Updates plus a log-local position; readTail replays a watermark. Also provides entity-level queries for materialization. |
| DirtyTracker | Tracks which entities need rematerialization. Queryable by type. |
| EntityStore | Materialized entity cache. Handles get (by ID) and query (by type). |
| CursorStore | Per-group GSN cursor tracking for sync resumption. |
| OutboxStore | Durable buffer of locally-authored Actions awaiting acknowledgement, keyed by action.id. |
| ReplicaStore | Single-slot persistence for the materialized replica (ReplicaRecord: format, position, revision, entities). A fast path only — an absent or corrupt record reads as null. |
| StorageAdapter | Unified interface composing all components. createMemoryAdapter() returns a full in-memory implementation. |
Usage
import { createMemoryAdapter } from "@ebbjs/storage/memory";
const storage = createMemoryAdapter();
// Append an action (marks affected entities dirty)
await storage.actions.append(action);
// Get a single entity (materializes if dirty)
const entity = await storage.entities.get("todo_1");
// Query all entities of a type
const todos = await storage.entities.query("todo");Architecture
Entities are materialized on-demand from the action log when they are dirty. This mirrors the server's materialized cache behavior — actions are the source of truth, entities are derived views.
Client receives action
│
▼
ActionLog.append()
│
▼
DirtyTracker.mark() — marks affected entity IDs dirty
│
▼
EntityStore.get() / .query() — materializes dirty entities
│
▼
Replays actions for each dirty entity, clears dirty flagConstraints
- Write path lives in
@ebbjs/client— the storage layer ships the durableOutboxStore, while flush policy, retry, and acknowledgement semantics belong to the client outbox - Conflict detection and resolution live in
@ebbjs/client— a detected LWW loser is aconflictedstatus plus payload on its own outbox row; storage ships only the durableOutboxStore, not a conflict table - Actions are append-only — never modified or deleted; rollbacks handled by compensating actions
- Materialization is lazy — entities only rematerialized when read, not eagerly on action receipt
- HLC + lexicographic tiebreak — same merge semantics as server (higher HLC wins, tiebreak by
update_id)
