@syrup-js/adapter-dexie
v0.0.2
Published
A powerful Dexie.js adapter for Syrup that enables seamless local-first development with automatic entity synchronization and persistence. This adapter integrates Syrup's entity system with Dexie.js, providing robust client-side storage with built-in sync
Readme
@syrup-js/adapter-dexie
A powerful Dexie.js adapter for Syrup that enables seamless local-first development with automatic entity synchronization and persistence. This adapter integrates Syrup's entity system with Dexie.js, providing robust client-side storage with built-in sync lifecycle management.
Architecture Overview
graph TD
subgraph Application
A[Client Code]
B[Entity Models]
R[Remote API Client]
end
subgraph Syrup Core
C[Entity System]
D[Sync Engine]
E[Decorators]
M[Entity Metadata]
end
subgraph Dexie Adapter
F[Middleware Layer]
G[Hook System]
H[Entity Serialization]
I[Query Transformation]
L[Debug Logger]
end
subgraph Storage
J[Dexie.js]
K[IndexedDB]
end
A --> B
B --> C
C --> D
C --> E
E --> M
F --> G
F --> H
F --> I
F --> L
B --> F
C --> F
D --> G
D --> R
F --> J
J --> K
classDef core fill:#f9f,stroke:#333,stroke-width:2px;
classDef adapter fill:#bbf,stroke:#333,stroke-width:2px;
classDef storage fill:#dfd,stroke:#333,stroke-width:2px;
class C,D,E,M core;
class F,G,H,I,L adapter;
class J,K storage;Features
- 🔄 Automatic entity serialization and deserialization
- 🎣 Built-in lifecycle hooks for sync operations
- 🛡️ Type-safe entity handling
- ⚡ Optimistic UI support
- 🔍 Transparent query result transformation
- 🎯 Zero-config entity table handling
- 🔌 Middleware-based integration with Dexie.js
- 🐛 Debug mode for development
Data Flow
flowchart TD
A[Client Code] --> B{Operation Type}
B -->|Query| C[Dexie Middleware]
B -->|Mutation| D[Dexie Middleware]
C --> E{Entity Table?}
D --> F{Entity Table?}
E -->|Yes| G[Execute Query]
E -->|No| H[Raw Query]
F -->|Yes| I[Serialize Data]
F -->|No| J[Raw Mutation]
G --> K[Wrap Results in Entity]
H --> L[Return Raw Data]
I --> M[Execute Mutation]
J --> N[Execute Raw Mutation]
K --> O[Final Result]
L --> O
M --> P{Is Create/Update?}
N --> O
P -->|Yes| Q[Execute Remote Sync]
P -->|No| O
Q --> O
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#dfd,stroke:#333,stroke-width:2px
style D fill:#dfd,stroke:#333,stroke-width:2px
style G fill:#ffd,stroke:#333,stroke-width:2px
style I fill:#ffd,stroke:#333,stroke-width:2px
style Q fill:#f9f,stroke:#333,stroke-width:2pxInstallation
npm install @syrup-js/adapter-dexie dexie@^4.0.11
# or
yarn add @syrup-js/adapter-dexie dexie@^4.0.11
# or
pnpm add @syrup-js/adapter-dexie dexie@^4.0.11Note: This package requires @syrup-js/core as a peer dependency.
Quick Start
import { createDexieAdapter } from '@syrup-js/adapter-dexie';
import Dexie, { type Table } from 'dexie';
import { Entity, entity, sync, type SyncState } from '@syrup-js/core';
import { apiClient } from './api';
// 1. Define your entity
@entity({
// If you have an API endpoint, you can use the endpoint property:
endpoint: '/api/todos',
// Otherwise, implement custom remote sync functions:
createFn: (todo) =>
apiClient.todos.create(todo.serialize({ removeSync: true })),
updateFn: (todo) =>
apiClient.todos.update(todo.serialize({ removeSync: true })),
// Optional debug mode
debug: true
})
export class Todo extends Entity {
@sync() text: string;
@sync() done: boolean;
@sync({ local: true }) lastViewed: Date; // Won't sync to remote
constructor(args: {
text: string;
done: boolean;
id?: string;
sync?: SyncState;
}) {
super();
this.text = args.text;
this.done = args.done;
this.id = args.id || crypto.randomUUID();
this.lastViewed = new Date();
if (args.sync) {
this.sync = args.sync;
}
}
}
// 2. Create your database
class TodoDb extends Dexie {
todos!: Table<Todo, string>;
constructor() {
super('TodoDb');
this.version(1).stores({
todos: '&id, text, done' // Define indexes
});
}
}
// 3. Initialize the adapter
const db = createDexieAdapter({
db: new TodoDb(),
entities: { todos: Todo }
});
export { db };Use Dexie's liveQuery for optimistic UI (svelte example below, but should work more or less the same in any framework)
<script lang="ts">
import {db} from '$lib/wherever'
const todosQuery = liveQuery(() => db.todos.toArray());
const todos = $derived($todosQuery ?? []);
let todoText = $state('');
let todoDone = $state(false);
</script>
<input type="text" bind:value={todoText} />
<input type="checkbox" bind:checked={todoDone} />
<button
onclick={() => {
// no need to call db.put!! instance is automatically created in Dexie
// and synced to remote whenever Todo is instantiated
new Todo({ text: todoText, done: todoDone });
todoText = '';
todoDone = false;
}}>Add</button
>
{#each todos as todo}
<div class="todo">
<div>ID: {todo.id}</div>
<div>Sync Status: {todo.sync.status}</div>
<div>
TEXT:
<input
type="text"
class={{ error: todo.sync.error?.type === 'validation' && todo.sync.error.fields.text }}
bind:value={
() => todo.text,
(value) => {
todo.text = value;
}
}
/>
</div>
<div>DONE: {todo.done}</div>
<button
onclick={() => {
todo.done = !todo.done;
}}>Toggle Done</button
>
{#if todo.sync.status === 'error' && todo.sync.error}
<div class="error">
<div>Error Type: {todo.sync.error.type}</div>
<div>Error Message: {todo.sync.error.message}</div>
</div>
{/if}
</div>
{/each}
Sync Lifecycle
sequenceDiagram
participant C as Client
participant E as Entity
participant H as Hooks
participant D as Dexie DB
participant R as Remote API
participant L as Debug Logger
Note over C,R: Normal Sync Flow
C->>E: Update Entity
alt Debug Mode Enabled
E->>L: Log Entity State
end
E->>H: Trigger beforeSync
H->>D: Save Initial State
alt Create/Update Operation
E->>R: Sync to Remote
alt Successful Sync
R-->>E: Sync Success
E->>H: Trigger afterSync
H->>D: Update Local State
else Sync Failed
R-->>E: Sync Error
E->>H: Trigger onError
H->>D: Save Error State
end
end
alt Debug Mode Enabled
E->>L: Log Final State
end
E-->>C: Return ResultAdvanced Usage
Working with Non-Entity Tables
Tables not configured with an entity class are treated as regular Dexie tables:
// Regular table operations work normally
await db.table('regular_table').put({ id: 1, data: 'test' });
// Entity tables get automatic wrapping and serialization
const todo = await db.table('todos').get(1); // Returns Todo instancePerformance Optimization
The adapter uses a middleware approach that adds minimal overhead:
- Entity wrapping occurs only for configured entity tables
- Serialization happens only during mutations
- Hooks are asynchronous and support concurrent operations
- Local-only properties skip remote sync
Best Practices
Entity Design
- Extend the
Entitybase class - Use
@sync()for properties that need synchronization - Use
@sync({ local: true })for local-only properties - Keep entities focused and single-responsibility
- Implement proper validation in constructors
- Include proper typing for constructor arguments
- Extend the
Remote Sync Implementation
- Implement both
createFnandupdateFn - Handle errors appropriately in sync functions
- Use
serialize({ removeSync: true })for remote APIs - Consider retry strategies for network issues
- Implement proper error mapping
- Implement both
Table Configuration
- Use clear, semantic table names
- Define appropriate indexes for query patterns
- Consider compound indexes for complex queries
- Document table schema in database class
- Match index definitions with queried properties
Troubleshooting
Common issues and solutions:
Entity Not Wrapping
- Verify entity extends
Entitybase class - Check
@entity()decorator is applied - Ensure table name matches configuration
- Verify entity class is exported
- Check constructor implementation
- Verify entity extends
Sync Hooks Not Firing
- Check
@sync()decorators are applied - Verify hook configuration
- Ensure async/await is used properly
- Check entity table configuration
- Verify
createFn/updateFnimplementation
- Check
Type Errors
- Update to latest TypeScript version
- Verify generic type constraints
- Check entity interface implementation
- Ensure proper type exports
- Verify constructor argument types
Debugging Tips
- Enable debug mode on entities
- Check browser console for logs
- Verify IndexedDB contents
- Monitor network requests
- Check entity state transitions
Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
MIT
