Jason Tudisco 6ebe02ad56 Initial commit: local-first browser sync library experiment
Four variants of the same sync library (IndexedDB, NeDB, SQLite WASM, sql.js)
plus a paste-bin demo app for testing multi-browser sync via shared folders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:04:08 -06:00

80 lines
2.7 KiB
Markdown

# IndexSyncFile — IndexedDB Variant
Local-first key-value and document store using **IndexedDB** as the local cache, syncing with a user-selected folder via the File System Access API.
## Why IndexedDB
- **Zero dependencies** — uses the browser's built-in IndexedDB API directly
- **Persistent by default** — IndexedDB data survives page reloads and browser restarts without any extra work
- **No WASM loading** — instant startup, no async module initialization
- **Battle-tested** — IndexedDB is the most widely supported browser storage API
## Architecture
```
Write: app ---> IndexedDB (immediate) ---> /events/timestamp_hash.json
Sync: /events/*.json ---> sort by timestamp ---> skip applied ---> apply to IndexedDB
```
IndexedDB serves as both the fast query layer and the local persistence layer. The folder's event log is the sync mechanism that keeps multiple browsers converged.
### IDB schema (5 object stores)
| Store | Key | Purpose |
|-------|-----|---------|
| `kv` | `key` | Key-value records |
| `docs` | `[store, id]` | Document collection records |
| `idx` | `[store, indexName, id]` | Secondary index entries |
| `applied` | `filename` | Tracks which event files have been processed |
| `meta` | `key` | Client ID, directory handle persistence |
The `FileSystemDirectoryHandle` is stored directly in the `meta` IDB store via structured clone, so the user doesn't need to re-select the folder on every page load.
## Install and build
```bash
npm install
npm run build
```
## Usage
```ts
import { FolderSyncDB } from './dist/index.js';
const db = await FolderSyncDB.open({
dbName: 'MyApp',
autoSyncIntervalMs: 5000,
});
await db.selectFolder();
// KV
await db.kv.set('user', { name: 'Alice' });
// Collections
const notes = db.collection({
name: 'notes',
indexes: [{ name: 'byTag', fields: ['tag'] }],
});
await notes.put({ id: 'n1', title: 'Hello', tag: 'work' });
// Exact match
const workNotes = await notes.findByIndex('byTag', 'work');
// Range query (IDB compound key ranges)
const recent = await notes.queryByIndex('byDate', { gte: '2024-01-01' });
```
## Index implementation
Secondary indexes are maintained in a dedicated `idx` IDB object store using compound keys `[store, indexName, id]`. An IDB index on `[store, indexName, value]` enables efficient exact-match and range queries via `IDBKeyRange`.
Index entries are updated atomically with document writes inside a single IDB transaction. On first use of a collection, indexes are rebuilt from existing documents.
## Variant-specific notes
- Directory handle is persisted in IDB (no separate storage needed)
- IDB transactions are used for atomic document + index updates
- No external dependencies