A TypeScript decorator-based ORM-like library for seamless data synchronization between local and remote stores. Perfect for building local-first applications with optimistic UI updates and robust error recovery.
- 📱 Local-First Architecture: Built for modern web apps that need to work offline and provide instant feedback
- 🔄 Automatic Synchronization: Handles data syncing between local and remote stores
- 🎯 Optimistic Updates: Implements optimistic UI patterns for better user experience
- 🛡️ Type-Safe: Full TypeScript support with decorator-based configuration
- ⚡ Efficient: Batches sync operations with configurable delays
- 🔄 State Management: Built-in sync state tracking with error recovery
- 🚀 Easy to Use: Simple decorator-based API for quick implementation
npm install @syrup-js/core
# or
yarn add @syrup-js/core
# or
pnpm add @syrup-js/core
- Define your entity:
import { Entity, entity, sync } from '@syrup-js/core';
@entity({
endpoint: '/api/users' // REST endpoint for sync
})
class User extends Entity {
@sync()
name: string;
@sync()
email: string;
@sync({ local: true }) // Won't be synced to server
lastLocalUpdate: Date;
constructor(name: string, email: string) {
super();
this.name = name;
this.email = email;
this.lastLocalUpdate = new Date();
}
}
- Use your entity:
// Create a new user
const user = new User('John Doe', 'john@example.com');
// Changes are automatically tracked and synced
user.name = 'John Smith';
// Check sync status
console.log(user.sync.status); // 'pending', 'synced', 'error'
All syncable entities must extend the Entity
base class and be decorated with @entity()
. This provides:
- Automatic sync state management
- Serialization utilities
- ID management
- Error tracking
Class decorator that marks a class as syncable. Configuration options:
interface EntityConfig<T> {
endpoint?: string; // REST API endpoint
createFn?: (entity: T) => Promise<void>; // Custom create function
updateFn?: (entity: T) => Promise<void>; // Custom update function
debug?: boolean; // Enable detailed logging
}
Property decorator that marks fields for synchronization:
interface SyncOptions {
local?: boolean; // If true, property won't be synced to server
}
Entities move through four possible states:
-
required
: Initial state, sync needed -
pending
: Sync in progress -
synced
: Successfully synchronized -
error
: Sync failed (includes error details)
The following diagram illustrates the sync state transitions:
stateDiagram-v2
[*] --> Required: Entity Created
Required --> Pending: Sync Initiated
Pending --> Synced: Sync Success
Pending --> Error: Sync Failed
Error --> Pending: Retry Sync
Synced --> Pending: Property Changed
note right of Required
Initial state for new entities
or after major changes
end note
note right of Pending
Entity queued for sync
Changes batched with delay
end note
note right of Synced
Successfully synced
with remote store
end note
note right of Error
Contains error details:
- Network errors
- Validation errors
- Server errors
end note
This diagram shows the complete sync process including hooks and error handling:
sequenceDiagram
participant C as Client
participant Q as Sync Queue
participant H as Hooks
participant R as Remote Store
Note over C: Property Changed
C->>Q: Enqueue Entity
activate Q
Note over Q: Wait for batch delay
Q->>H: Execute beforeSync
activate H
H-->>Q: Hooks Complete
deactivate H
Q->>R: Sync to Remote
activate R
alt Sync Success
R-->>Q: Success Response
Q->>H: Execute afterSync
activate H
H-->>Q: Hooks Complete
deactivate H
Q->>C: Update State to Synced
else Sync Error
R-->>Q: Error Response
Q->>H: Execute onError
activate H
H-->>Q: Hooks Complete
deactivate H
Q->>C: Update State to Error
end
deactivate R
deactivate Q
The following class diagram shows the key components and their relationships:
classDiagram
class Entity {
+string id
+SyncState sync
+serialize()
}
class SyncState {
+string status
+number lastAttempt
+number lastSuccess
+SyncError error
+SyncTransition[] transitions
}
class SyncQueue {
-Map queue
-number delay
+enqueue(entity)
+processQueue()
-scheduleProcessing()
}
class EntityConfig {
+string endpoint
+function createFn
+function updateFn
+boolean debug
}
class SyncError {
+string type
+string message
+number timestamp
+any details
}
Entity "1" -- "1" SyncState: has
Entity "0..*" -- "1" SyncQueue: managed by
Entity "0..*" -- "1" EntityConfig: configured by
SyncState "1" -- "0..1" SyncError: contains
This flowchart illustrates how syrup-js makes decisions about sync operations:
flowchart TD
A[Property Changed] --> B{Is Local Only?}
B -->|Yes| C[Skip Sync]
B -->|No| D{Current State?}
D -->|Required/Error| E[Create Operation]
D -->|Synced| F[Update Operation]
D -->|Pending| G[Already Queued]
E --> H{Has Endpoint?}
F --> H
H -->|Yes| I[Use REST API]
H -->|No| J{Has Custom Fn?}
J -->|Yes| K[Use Custom Function]
J -->|No| L[Throw Config Error]
I --> M[Queue Operation]
K --> M
M --> N{Queue Size?}
N -->|1| O[Process Immediately]
N -->|>1| P[Batch with Delay]
Comprehensive error types for different scenarios:
// Network errors (including offline detection)
interface NetworkError {
type: 'network';
message: string;
offline?: boolean;
timestamp: number;
}
// Validation errors
interface ValidationError {
type: 'validation';
message: string;
fields: Record<string, string[]>;
timestamp: number;
}
// Server errors
interface ServerError {
type: 'server';
message: string;
code: number;
timestamp: number;
details?: Record<string, unknown>;
}
Example error handling:
if (user.sync.status === 'error') {
const error = user.sync.error;
switch (error.type) {
case 'network':
if (error.offline) {
// Handle offline scenario
}
break;
case 'validation':
// Handle validation errors
const fieldErrors = error.fields;
break;
case 'server':
// Handle server errors
console.error(`Server error ${error.code}: ${error.message}`);
break;
}
}
Instead of REST endpoints, you can provide custom sync functions:
@entity({
createFn: async (user) => {
// Custom create logic
await api.createUser(user);
},
updateFn: async (user) => {
// Custom update logic
await api.updateUser(user);
}
})
class User extends Entity {
// ... entity implementation
}
The sync queue batches changes with a configurable delay:
@entity({
endpoint: '/api/users',
queueDelay: 500 // Customize delay (default: 300ms)
})
Enable detailed logging for troubleshooting:
@entity({
endpoint: '/api/users',
debug: process.env.NODE_ENV === 'development'
})
-
Error Handling
- Always implement error handling for sync operations
- Use structured error types for better error management
- Test offline scenarios and network failures
-
Performance
- Configure appropriate queue delays based on your use case
- Use local-only properties for data that doesn't need syncing
- Be mindful of network requests in your sync functions
-
Type Safety
- Use TypeScript for better development experience
- Leverage type checking for sync states and errors
- Define proper interfaces for your entities
-
State Management
- Monitor sync states for proper error handling
- Use state transitions appropriately
- Implement retry logic for failed syncs
syrup-js is particularly well-suited for:
-
Collaborative Applications
- Real-time document editors
- Project management tools
- Team collaboration platforms
- Any app requiring immediate feedback with background sync
-
Offline-First Applications
- Mobile web applications
- Progressive Web Apps (PWAs)
- Field service applications
- Data collection tools
-
High-Latency Environments
- Applications used in areas with poor connectivity
- Systems requiring immediate user feedback
- Applications with complex backend processing
-
Complex Form Handling
- Multi-step forms with draft saving
- Applications with periodic auto-save
- Forms with offline support
-
Data-Heavy Applications
- CRM systems
- Inventory management
- Analytics dashboards with local caching
We welcome contributions! Please see our Contributing Guide for details.
MIT License - see LICENSE for details.