Store

Mode

Skin

Theme

Overview & Philosophy

ln-data-store is applied to a hidden element via data-ln-data-store id="name" to provide client-side IndexedDB persistence with automatic schema indexing and optional AES-GCM encryption. It is completely decoupled from the network: responding to request events (ln-data-store:request-query, request-save, request-delete) against local records and broadcasting ln-data-store:stale when cached data expires, allowing applications to function offline and render cached data instantly.

In ln-ashlar, this local-first cache layer decouples UI responsiveness from network latency, treating IndexedDB as the immediate source of truth for presentation components.

The Local-First Doctrine

A local-first store never forces the UI to await network requests. Write actions are committed locally to IndexedDB instantly and dispatched to the interface, while background queues (with exponential backoff and version conflict resolution) sync mutations with the server. The local IndexedDB cache is the single source of truth for the UI.

Three Orthogonal Layers of the Data Pipeline

  1. The Cache Storage (IndexedDB): A native, highly-performant browser database capable of storing megabytes of structured JSON data. It supports indexing, fast text-based search queries, and stays persisted fully across app restarts and offline sessions.
  2. The Sync Engine (Delta Protocol): A background service that communicates with server API endpoints. It tracks timestamps, fetches only records changed since the last sync (delta arrays), processes conflict resolutions, and maintains outbox write queues.
  3. The Presentation Layer (UI Elements): Data tables (ln-table) and select controls render data directly from local IndexedDB queries. They listen to bubbled store events (like ln-data-store:created, ln-data-store:updated) to update lists in real-time.

How the Data Flow Coherence Works

When a record is created or mutated by the user:

  1. The coordinator claims the form submit and dispatches a local write event (e.g. ln-data-store:request-create) with the form payload and a temporary ID.
  2. The store saves the record locally and instantly dispatches ln-data-store:created. Sibling views receive the event and display the new record immediately.
  3. In the background, the coordinator pushes the network request via the connector or outbox queue. On server confirmation, the coordinator dispatches an ordinary ln-data-store:request-update to rekey the temporary ID to the server ID (emitting ln-data-store:updated). On a 4xx create rejection, it dispatches ln-data-store:request-delete (emitting ln-data-store:deleted).

For a detailed breakdown of local-first patterns, see the Data Flow Architecture, the ln-data-store README, and the architecture reference.

Attributes

AttributeRequiredDescription
data-ln-data-storeyesStore name (IndexedDB object store name, event namespace)
data-ln-data-store-stalenoStaleness threshold in seconds (default: 300). "0" = always stale (syncs on every mount). "-1" or "never" = never auto-sync (only forceSync())
data-ln-data-store-indexesnoComma-separated fields to index
data-ln-data-store-search-fieldsnoComma-separated fields for text search

Live Demo — Mock Store

This demo simulates ln-store behavior using an in-memory mock (no real IndexedDB or server). The event log shows the lifecycle.

Store State

PropertyValue
isLoadedfalse
totalCount0
isSyncingfalse

Records

    Event Log

    
    
    							

    For a live CRUD wiring example with table + create/edit modal + 409-conflict handling, see Store — Usecase.

    Events — Commands (dispatched TO the store)

    EventdetailDescription
    ln-data-store:request-create{ data }Create record (optimistic with temp ID)
    ln-data-store:request-update{ id, data, expected_version }Update record (optimistic, conflict detection)
    ln-data-store:request-delete{ id }Delete record (optimistic)
    ln-data-store:request-bulk-delete{ ids }Delete multiple records (optimistic)

    Events — Notifications (emitted BY the store)

    Data Events

    EventWhendetail
    ln-data-store:readyData available{ store, count, source }
    ln-data-store:loadedInitial full load{ store, count }
    ln-data-store:syncedDelta sync complete{ store, added, deleted, changed: bool }

    Mutation Events

    EventWhendetail
    ln-data-store:createdOptimistic create{ store, record, tempId }
    ln-data-store:updatedOptimistic update{ store, record, previous }
    ln-data-store:deletedOptimistic delete{ store, id } or { store, ids }
    ln-data-store:confirmedServer confirmed{ store, record, action }
    ln-data-store:revertedMutation failed{ store, record, action, error }

    Error Events

    EventWhendetail
    ln-data-store:errorInitial load failed{ store, action, error, status }
    ln-data-store:offlineServer unreachable{ store }

    Sync Lifecycle

    Init:
      1. Open IndexedDB → read _meta
      2. Schema mismatch? → clear, full load
      3. Has cache? → emit ready (source: 'cache'), delta sync if stale
      4. No cache? → full load → emit loaded, ready (source: 'server')
    
    Visibility change (tab returns):
      → delta sync all stores if stale
    
    Delta sync:
      1. GET {endpoint}?since={last_synced_at}
      2. Upsert data[], delete deleted[]
      3. Emit synced

    Optimistic Mutation Flow

    Create:  insert temp → POST → confirmed | reverted
    Update:  save prev, update → PUT → confirmed | conflict | reverted
    Delete:  save, remove → DELETE → confirmed | reverted
    Bulk:    save all, remove → DELETE /bulk-delete → confirmed | reverted

    API

    const storeEl = document.getElementById('documents');
    
    // Queries (all return Promise)
    storeEl.lnDataStore.getAll(options)        // → { data, total, filtered }
    storeEl.lnDataStore.getById(id)            // → Object|null
    storeEl.lnDataStore.count(filters)         // → Number
    storeEl.lnDataStore.aggregate(field, fn)   // → Number (sum, avg, count)
    
    // State
    storeEl.lnDataStore.isLoaded               // Boolean
    storeEl.lnDataStore.isSyncing              // Boolean
    storeEl.lnDataStore.lastSyncedAt           // Number|null
    storeEl.lnDataStore.totalCount             // Number
    
    // Manual triggers
    storeEl.lnDataStore.forceSync()            // delta sync
    storeEl.lnDataStore.fullReload()           // clear + full reload
    storeEl.lnDataStore.destroy()              // cleanup
    
    // Global
    window.lnDataStore.clearAll()              // wipe all stores (logout)
    window.lnDataStore.init(el)                // manual init

    Query Options

    storeEl.lnDataStore.getAll({
        sort: { field: 'title', direction: 'asc' },
        filters: { status: ['approved', 'draft'] },
        search: 'ISO 27001',
        offset: 0,
        limit: 100
    })
    // → { data: [...], total: 1247, filtered: 45 }

    Server Response Format

    // Full load / Delta sync
    {
        "data": [{ "id": 42, "title": "...", "updated_at": 1736952600 }],
        "deleted": [17, 23],          // delta only
        "synced_at": 1736953500       // next ?since= value
    }

    Reconnect auto-resync

    When the browser goes back online, ln-data-store automatically triggers a remote sync for all loaded stores — default on, no wiring required. The isSyncing guard makes it idempotent alongside manual forceSync() calls.

    Opt out per store: <div data-ln-data-store id="chat" data-ln-data-store-no-autosync></div>

    Connectivity indicator (listens to ln-data-store:online / ln-data-store:offline on document):

    Toggle your network connection to see the indicator update.