asljs-dali
v0.1.4
Published
IndexedDB data layer with a typed Table abstraction.
Maintainers
Readme
dali
Part of Alexandrite Software Library - a set of high-quality, performant JavaScript libraries for everyday use.
Overview
asljs-dali is a data layer for apps that store data in IndexedDB. It is for
developers who want a typed, event-aware table abstraction instead of
hand-writing low-level request and transaction plumbing. Use it to model
stores as Table<T>, keep CRUD operations consistent, and optionally enforce
optimistic concurrency with version strategies.
Installation
npm install asljs-daliNPM Package: asljs-dali
Package Concept Map
dbOpen(...),dbDelete(...), anddbRequestAsync(...)manage database setup and low-level request handling.Table<T>is the main high-level abstraction for typed IndexedDB work.notify(...)andobserve(...)handle committed change notifications.record(...)andrecordset(...)provide live-first containers.- Transaction helpers support lower-level control when
Table<T>is not the right layer. - Version and delete strategies customize concurrency and deletion behavior.
Choose This API When
- If you need a one-time single-row read, then use
getOne(key). - If you need a one-time filtered scan, then use
scan(predicate). - If you need live single-row tracking, then use
record(key). - If you need live filtered tracking, then use
recordset(predicate). - If you need local-only mutation notifications, then use
notify(...). - If you need local-plus-remote committed notifications, then use
observe(...).
Public Contracts
notify(...)is local-only.observe(...)includes local and remote committed changes.- Broadcasts happen only after a successful commit.
- Remote messages are not re-published.
record(key)is key-based only.recordset(predicate)is client-side predicate filtering only.
What This Package Does Not Provide
- No joins.
- No server-style query planners.
- No DB-level query composition through
recordset(...). - No automatic ordering semantics for live sets.
- No re-publishing of remote messages.
Public Surface Summary
DB helpers:
dbOpendbDeletedbRequestAsync
Tables and live views:
TableLiveRecordLiveRecordSet
Version and delete strategies:
IncrementTableVersionStrategyUuidTableVersionStrategyTableVersionStrategyTableVersionConflictErrorTableDeleteStrategyUuidSoftDeleteTableDeleteStrategy
Transaction helpers:
txReadtxWritetxDonetxEnsuretxReuseOrCreateTxMode
Broadcast and observe types:
TableBroadcastServiceTableBroadcastMessageTableObservedEventTableObservedReceiver
Event-source and saga helpers:
EventSourceManagerIndexedDbEventSourceAdapterEventSourceProjectionManagerSagaManager- setup and store helper exports from event-source and saga modules
Usage
import {
dbOpen,
Table,
} from 'asljs-dali';
type Note =
{ id: string;
title: string; };
const db =
await dbOpen(
'notes-db',
[ targetDb => {
targetDb.createObjectStore(
'notes',
{ keyPath: 'id' });
} ]);
const notes =
new Table<Note>(
'notes',
db,
{ /* options */ });
await notes.add(
{ id: '1',
title: 'Hello' });
const row =
await notes.getOne('1');Cross-tab notifications with observe()
Table supports two notification paths:
- If you want callbacks only for writes committed by this
Tableinstance, then usenotify(receiver). - If you want callbacks for local writes and remote writes from other tabs,
then use
observe(receiver).
Pass a broadcastService to the Table constructor to enable cross-tab delivery.
The service is an abstraction — you can implement it with BroadcastChannel or
any equivalent transport.
import {
type TableBroadcastMessage,
type TableBroadcastService,
} from 'asljs-dali';
// BroadcastChannel-backed implementation
function makeBroadcastService(
channelName: string
): TableBroadcastService
{
const channel = new BroadcastChannel(channelName);
return {
publish(message: TableBroadcastMessage) {
channel.postMessage(message);
},
subscribe(handler) {
const listener = (ev: MessageEvent) => handler(ev.data);
channel.addEventListener('message', listener);
return () => channel.removeEventListener('message', listener);
},
};
}
const notes =
new Table<Note>(
'notes',
db,
{ broadcastService: makeBroadcastService('notes-sync') });
// Local-only — fires only for writes made by this Table instance.
notes.notify(
{ add(record) { console.log('local add', record); } });
// Observed — fires for both local and remote writes.
// The `source` field tells you where the change came from.
const unobserve =
notes.observe(event => {
console.log(event.source, event.eventType);
if (event.eventType === 'add')
console.log(event.record);
});
// When the Table is no longer needed, dispose it to stop listening.
notes.dispose();Design rules:
- Broadcast messages are published only after a successful IndexedDB transaction; rolled-back or provisional changes are never broadcast.
- A Table instance discards its own echoed messages using a per-instance
originIdincluded in every broadcast message. - Remote messages are routed only to
observe()subscribers; local-onlynotify()subscribers are never called for remote events. - A Table receiving a remote message does not re-publish it, preventing broadcast loops.
Live views with record() and recordset()
Table provides live-first APIs that return reactive containers tracking
committed table changes automatically. Both containers are built on
ASLJS eventful (for domain events) and ASLJS observable (for
property-path watching).
Table.record(key) → LiveRecord<T>
Returns a live single-record view for a specific primary key.
const live = notes.record('1');
// Stable property — null until the initial load settles.
console.log(live.record); // { id: '1', title: 'Hello' } | null
// Domain events via ASLJS eventful.
live.on('changed', (record, previous) => {
console.log('record changed to', record, 'was', previous);
});
live.on('deleted', previous => {
console.log('record deleted, was', previous);
});
// Property-path watching via ASLJS observable.
live.watch('record.title', title => {
console.log('title is now', title);
});
// Release the live view when no longer needed.
live.dispose();Behaviour:
recordisnulluntil the initial database read settles.- On
add/updatefor the tracked key —recordis updated andchangedfires. - On
deleteorclear—recordbecomesnullanddeletedfires. - Unrelated changes on the same table do not affect this view.
watch(path, cb)is called immediately with the current value and again whenever the path changes. Watchers are anchored to the stable container.
Snapshot read: use
table.getOne(key)instead.Limitation:
record(key)is limited to key-only semantics.
Table.recordset(predicate) → LiveRecordSet<T>
Returns a live filtered set view for records matching a client-side predicate.
const live = notes.recordset(note => note.title.startsWith('A'));
// Stable property — a readonly array snapshot.
console.log(live.records); // readonly Note[]
// Domain events via ASLJS eventful.
live.on('added', record => console.log('added', record));
live.on('removed', record => console.log('removed', record));
live.on('updated', (record, prev) => console.log('updated', record, prev));
live.on('cleared', () => console.log('cleared'));
live.on('changed', records => console.log('set now has', records.length));
// Property-path watching via ASLJS observable.
live.watch('records', records => {
console.log('count:', (records as readonly Note[]).length);
});
live.dispose();Behaviour:
- On initial creation the table is scanned and all matching records are loaded.
- On
add— the record is included if the predicate returnstrue;addedfires. - On
update— membership is re-evaluated;added,updated, orremovedfires accordingly. - On
delete— the record is removed if it was present;removedfires. - On
clear— the set is emptied andclearedfires. changedfires after every mutation.
Snapshot read: use
table.scan(predicate)instead.Limitation:
recordset(predicate)is limited to client-side predicate semantics. Joins, ordering, and DB-level query composition are not supported.
API Reference
Core:
dbOpen(name, upgrades)dbDelete(name)dbRequestAsync(request)Table<T>
Live views:
LiveRecord<T>— live single-record container returned byTable.record(key)- Events (ASLJS eventful):
changed,deleted - Watch (ASLJS observable):
record.someField
- Events (ASLJS eventful):
LiveRecordSet<T>— live filtered set container returned byTable.recordset(predicate)- Events (ASLJS eventful):
added,removed,updated,cleared,changed - Watch (ASLJS observable):
records,records.length
- Events (ASLJS eventful):
LiveRecordEvents<T>— event map type forLiveRecordLiveRecordSetPayload<T>—set:record/setevent payload forLiveRecordLiveRecordSetEvents<T>— event map type forLiveRecordSetLiveRecordSetSetPayload<T>—set:records/setevent payload forLiveRecordSet
Versioning:
TableVersionStrategy<T>TableVersionConflictErrorIncrementTableVersionStrategy<T>UuidTableVersionStrategy<T>
Delete strategies:
TableDeleteStrategy<T>UuidSoftDeleteTableDeleteStrategy<T>
Transactions:
TxModetxRead(db, storeName, tx?)txWrite(db, storeName, tx?)txDone(tx)txEnsure(tx, storeName, mode)txReuseOrCreate(tx, storeNames, mode, db)
Broadcast / cross-tab:
TableBroadcastService— interface for the publish/subscribe transportTableBroadcastMessage— message shape published on every committed changeTableObservedEvent<T>— event delivered toobserve()subscribersTableObservedReceiver<T>— callback type forobserve()
Common Wrong Assumptions
recordset(predicate)is a database query planner.notify(...)includes remote tab changes.observe(...)re-broadcasts remote changes.- live views imply joins or rich query composition.
- broadcast delivery happens during tentative mutations instead of after commit.
Related Packages
- For event primitives, see
asljs-eventful. - For path watching and reactive property access, see
asljs-observable. - For DOM binding on top of observable models, see
asljs-data-binding.
Safe Usage Rules
- Use
Table<T>before dropping to raw transaction helpers. - Prefer snapshot reads unless reactivity is actually needed.
- Use
observe(...)only when remote-origin changes matter. - Dispose live views when they are no longer needed.
- Do not describe
recordset(predicate)as a full query engine.
License
MIT License. See LICENSE for details.
