Table Store
The Table Store in Anchor provides a structured storage solution backed by IndexedDB with support for complex queries, indexing, and relational-like operations. It's designed for scenarios where you need to store collections of structured data with advanced querying capabilities.
Overview
The Table Store functionality is provided through the createTable
function. It offers an optimistic API for IndexedDB operations with built-in support for indexing, querying, and reactive state management.
API
createTable()
Function
Creates a reactive table instance that provides state management for IndexedDB records.
type createTable = <T extends Rec, R extends Row<T> = Row<T>>(
name: string,
version = 1,
indexes?: (keyof R)[],
remIndexes?: (keyof R)[],
dbName = name,
seeds?: R[]
) => ReactiveTable<T, R>;
Parameters:
name
- The name of the IndexedDB object storeversion
- The version of the database schemaindexes
- An array of index names to create in the object storeremIndexes
- An array of index names to remove from the object storedbName
- The name of the databaseseeds
- An array of seed data to populate the object store
Returns: A reactive table interface with methods for managing records
ReactiveTable
Interface
ReactiveTable interface provides a reactive wrapper around IndexedTable operations.
Methods
.get(id)
Gets a reactive row state by ID.
get: (id: string) => RowState<R>;
Parameters:
id
- The record ID to fetch
Returns: RowState containing the reactive data and status
.add(payload)
Adds a new record to the table.
add: (payload: T) => RowState<R>;
Parameters:
payload
- The record data to create
Returns: RowState containing the reactive data and status
.list(filter?, limit?, direction?)
Lists records matching the filter criteria.
list: (filter?: IDBKeyRange | FilterFn<R>, limit?: number, direction?: IDBCursorDirection) => RowListState<R>;
Parameters:
filter
- The filter criteria (IDBKeyRange or FilterFn) (optional)limit
- Maximum number of records to return (default: 25)direction
- Cursor direction (optional)
Returns: RowListState containing the reactive data array and status
.listByIndex(name, filter?, limit?, direction?)
Lists records by index matching the filter criteria.
listByIndex: (name: keyof R, filter?: IDBKeyRange | FilterFn<R>, limit?: number, direction?: IDBCursorDirection) =>
RowListState<R>;
Parameters:
name
- The index name to search onfilter
- The filter criteria (IDBKeyRange or FilterFn) (optional)limit
- Maximum number of records to return (default: 25)direction
- Cursor direction (optional)
Returns: RowListState containing the reactive data array and status
.remove(id)
Removes a record by ID.
remove: (id: string) => RowState<R>;
Parameters:
id
- The record ID to delete
Returns: RowState containing the reactive data and status
.seed(seeds)
Seeds the table with initial data.
seed: <T extends R[]>(seeds: T) => this;
Parameters:
seeds
- An array of records to seed the table with
Returns: The current ReactiveTable instance for method chaining
.leave(id)
Decrements the reference count for a row and cleans up if no longer used.
leave: (id: string) => void
Parameters:
id
- The record ID to leave
.promise(state)
Convert the state into a promise that resolves when the state is ready.
promise: <T extends RowState<R> | RowListState<R>>(state: T) => Promise<T>;
Parameters:
state
- The state to wait for completion
Returns: A promise that resolves when the state is completed
Usage Examples
Basic Usage
import { createTable } from '@anchorlib/storage/db';
// Create a reactive table
const userTable = createTable('users', 1, ['email', 'age']);
// Add a new record
const newUser = userTable.add({
name: 'John Doe',
email: 'john@example.com',
age: 30,
});
// The state has a status that indicates the operation progress
console.log(newUser.status); // 'pending' -> 'ready' | 'error'
// Wait for the operation to complete
await userTable.promise(newUser);
// Get an existing record
const existingUser = userTable.get(newUser.data.id);
console.log(existingUser.status); // 'pending' -> 'ready' | 'error'
// Update a record
existingUser.data.name = 'John Smith';
// Remove a record
const removedUser = userTable.remove(newUser.data.id);
Querying Data
import { createTable } from '@anchorlib/storage/db';
type Product = {
name: string;
price: number;
category: string;
};
// Create table with indexes for querying
const productTable = createTable<Product>('products', 1, ['price', 'category']);
// Add sample data
const laptop = productTable.add({ name: 'Laptop', price: 1000, category: 'Electronics' });
const phone = productTable.add({ name: 'Phone', price: 500, category: 'Electronics' });
const book = productTable.add({ name: 'Book', price: 20, category: 'Education' });
// Wait for operations to complete
await Promise.all([productTable.promise(laptop), productTable.promise(phone), productTable.promise(book)]);
// List all products
const allProducts = productTable.list();
await productTable.promise(allProducts);
console.log(allProducts.data.length); // 3
// List products with a filter function
const expensiveProducts = productTable.list((product) => product.data.price > 100);
await productTable.promise(expensiveProducts);
console.log(expensiveProducts.data.length); // 2
// List products by index
const electronics = productTable.listByIndex('category', IDBKeyRange.only('Electronics'));
await productTable.promise(electronics);
console.log(electronics.data.length); // 2
// List with count (useful for pagination)
const productList = productTable.list(
(product) => product.data.price < 1000,
10 // limit
);
await productTable.promise(productList);
console.log(productList.count); // Total matching records
console.log(productList.data.length); // Records returned (up to limit)
Working with Indexes
import { createTable } from '@anchorlib/storage/db';
type Order = {
customerId: string;
amount: number;
status: 'pending' | 'shipped' | 'delivered';
};
// Create a table with multiple indexes
const orderTable = createTable<Order>('orders', 1, ['customerId', 'amount', 'status']);
// Add sample data
const order1 = orderTable.add({ customerId: 'cust1', amount: 100, status: 'pending' });
const order2 = orderTable.add({ customerId: 'cust1', amount: 200, status: 'shipped' });
const order3 = orderTable.add({ customerId: 'cust2', amount: 150, status: 'delivered' });
// Wait for operations to complete
await Promise.all([orderTable.promise(order1), orderTable.promise(order2), orderTable.promise(order3)]);
// List by customer ID
const customerOrders = orderTable.listByIndex('customerId', IDBKeyRange.only('cust1'));
await orderTable.promise(customerOrders);
// List by amount range
const highValueOrders = orderTable.listByIndex(
'amount',
IDBKeyRange.lowerBound(150) // >= 150
);
await orderTable.promise(highValueOrders);
// List by status
const pendingOrders = orderTable.listByIndex('status', IDBKeyRange.only('pending'));
await orderTable.promise(pendingOrders);
Seeding Data
import { createTable } from '@anchorlib/storage/db';
type Country = {
name: string;
code: string;
population: number;
};
// Create table with seed data
const countryTable = createTable<Country>('countries', 1, ['code'], undefined, 'geo-db', [
{
id: '1',
name: 'United States',
code: 'US',
population: 331000000,
created_at: new Date(),
updated_at: new Date(),
},
{
id: '2',
name: 'Canada',
code: 'CA',
population: 38000000,
created_at: new Date(),
updated_at: new Date(),
},
]);
// The seed data will be inserted only if the table is empty
// This is useful for initializing databases with default data
// List seeded data
const countries = countryTable.list();
await countryTable.promise(countries);
console.log(countries.data.length); // 2
Best Practices
1. Schema Design
Design your schemas carefully to optimize for your query patterns:
import { createTable } from '@anchorlib/storage/db';
// Good: Include indexes for frequently queried fields
type User = {
email: string; // Frequently searched
department: string; // Frequently filtered
createdAt: Date; // Frequently sorted
isActive: boolean; // Frequently filtered
};
const userTable = createTable<User>(
'users',
1,
['email', 'department', 'createdAt', 'isActive'] // Indexes for common queries
);
2. Error Handling
Always handle asynchronous operations properly:
import { createTable } from '@anchorlib/storage/db';
type Product = {
name: string;
price: number;
};
const productTable = createTable<Product>('products');
// Good: Handle errors properly
const productState = productTable.add({
name: 'New Product',
price: 29.99,
});
// Wait for the operation and check for errors
try {
await productTable.promise(productState);
console.log('Product created:', productState.data);
} catch (error) {
console.error('Failed to create product:', error);
// Implement fallback behavior
}
// Also handle list errors
const productList = productTable.list((product) => product.data.price > 10);
try {
await productTable.promise(productList);
console.log('Found products:', productList.data.length);
} catch (error) {
console.error('Failed to query products:', error);
}
3. Pagination
Use the list methods for efficient pagination:
import { createTable } from '@anchorlib/storage/db';
type Article = {
title: string;
content: string;
publishedAt: Date;
};
const articleTable = createTable<Article>('articles', 1, ['publishedAt']);
// Implement pagination
async function getArticles(page = 1, pageSize = 10) {
// Get data
const articleList = articleTable.list(
undefined, // No filter
pageSize, // Limit
'prev' // Direction (optional)
);
// Wait for data to load
await articleTable.promise(articleList);
return {
articles: articleList.data,
total: articleList.count,
page,
pageSize,
};
}
// Usage
const page1 = await getArticles(1, 10);
console.log(`Showing ${page1.articles.length} of ${page1.total} articles`);
Browser Compatibility
IndexedDB is supported in all modern browsers:
- Chrome 24+
- Firefox 16+
- Safari 10+
- Edge 12+
- Internet Explorer 10+ (with prefixes)
In environments where IndexedDB is not available, operations will fail gracefully with appropriate error messages.
Performance Considerations
Indexing: Create indexes only for fields you frequently query. Each index has a storage and performance cost.
Batch Operations: For inserting multiple records, consider using Promise.all or sequential operations rather than parallel operations which can overwhelm the database.
Query Limits: Use the limit parameter to avoid loading too much data at once.
Cursor Direction: Use cursor direction parameters to optimize sorted queries.
Memory Usage: Row data is cached in memory for reactive tables. Be mindful of storing large datasets in reactive tables.