Skip to content

synclib-io/synclib-js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

synclib

TypeScript/JavaScript wrapper for SQLite with automatic change tracking for syncing. This library uses sql.js (SQLite compiled to WebAssembly) and automatically tracks all write operations, making it easy to sync data across devices and clients.

Features

  • 🔄 Automatic Change Tracking - All write operations are automatically tracked for syncing
  • 💾 IndexedDB Persistence - Data is automatically persisted to IndexedDB in the browser
  • 🌐 Web Compatible - Works in all modern browsers using WebAssembly
  • 📦 TypeScript Support - Full TypeScript definitions included
  • 🎯 Framework Agnostic - Works with React, Vue, Svelte, or vanilla JS
  • 🔒 Schema Versioning - Built-in schema version management

Installation

npm install synclib
# or
pnpm add synclib
# or
yarn add synclib

Quick Start

import { SynclibDatabase, SynclibOperation } from 'synclib';

// Open a database
const db = await SynclibDatabase.open({
  databaseName: 'myapp.db'
});

// Create a table
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL
  )
`);

// Insert data with change tracking
await db.write({
  tableName: 'users',
  rowId: '123',
  operation: SynclibOperation.INSERT,
  sql: `INSERT INTO users (id, name, email) VALUES (?, ?, ?)`,
  params: ['123', 'John Doe', 'john@example.com'],
  data: JSON.stringify({ id: '123', name: 'John Doe', email: 'john@example.com' })
});

// Read data
const users = db.read('SELECT * FROM users');
console.log(users);

// Get pending changes to sync
const changes = db.getPendingChanges();
console.log('Changes to sync:', changes);

// After syncing, mark changes as synced
if (changes.length > 0) {
  await db.markSynced(changes[changes.length - 1].seqnum);
}

Usage with Svelte

Here's an example of using synclib in a Svelte component:

<script lang="ts">
  import { onMount } from 'svelte';
  import { SynclibDatabase, SynclibOperation } from 'synclib';

  let db: SynclibDatabase | null = null;
  let users: Array<{ id: string; name: string; email: string }> = [];

  onMount(async () => {
    // Open database
    db = await SynclibDatabase.open({
      databaseName: 'myapp.db'
    });

    // Create table if not exists
    db.exec(`
      CREATE TABLE IF NOT EXISTS users (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT NOT NULL
      )
    `);

    // Load users
    loadUsers();
  });

  function loadUsers() {
    if (!db) return;
    users = db.read('SELECT * FROM users ORDER BY name');
  }

  async function addUser(name: string, email: string) {
    if (!db) return;

    const id = crypto.randomUUID();

    await db.write({
      tableName: 'users',
      rowId: id,
      operation: SynclibOperation.INSERT,
      sql: `INSERT INTO users (id, name, email) VALUES (?, ?, ?)`,
      params: [id, name, email],
      data: JSON.stringify({ id, name, email })
    });

    loadUsers();
  }

  async function syncChanges() {
    if (!db) return;

    // Get pending changes
    const changes = db.getPendingChanges();

    // Send changes to server
    const response = await fetch('/api/sync', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ changes })
    });

    if (response.ok) {
      const { lastSeqnum } = await response.json();
      await db.markSynced(lastSeqnum);
    }
  }
</script>

<div>
  <h1>Users</h1>

  {#each users as user}
    <div>{user.name} - {user.email}</div>
  {/each}

  <button on:click={syncChanges}>Sync Changes</button>
</div>

API Reference

SynclibDatabase.open(config)

Opens a database connection.

Parameters:

  • config.databaseName (string): Name of the database
  • config.wasmUrl (string, optional): URL to sqlite3.wasm file

Returns: Promise<SynclibDatabase>

db.exec(sql)

Executes SQL without change tracking (for DDL, schema changes, etc.).

Parameters:

  • sql (string): SQL statement to execute

db.read(sql)

Executes a read-only query.

Parameters:

  • sql (string): SQL SELECT statement

Returns: Array<Record<string, any>>

db.write(options)

Executes a write operation with change tracking.

Parameters:

  • options.tableName (string): Name of the table being modified
  • options.rowId (string): Primary key of the row
  • options.operation (SynclibOperation): Operation type (INSERT, UPDATE, DELETE)
  • options.sql (string): SQL statement to execute
  • options.params (any[], optional): Parameters for the SQL statement
  • options.data (string, optional): JSON-encoded row data for tracking

Returns: Promise<void>

db.applyRemote(options)

Applies a remote change without creating a local change record (prevents sync loops).

Parameters:

  • options.tableName (string): Name of the table being modified
  • options.rowId (string): Primary key of the row
  • options.operation (SynclibOperation): Operation type
  • options.sql (string): SQL statement to execute
  • options.params (any[], optional): Parameters for the SQL statement
  • options.data (string, optional): JSON-encoded row data

Returns: Promise<void>

db.getPendingChanges(limit?)

Gets pending changes that haven't been synced.

Parameters:

  • limit (number, optional): Maximum number of changes to retrieve (default: 100)

Returns: Change[]

db.markSynced(seqnum)

Marks changes as synced up to the given sequence number.

Parameters:

  • seqnum (number): Sequence number up to and including which to mark as synced

Returns: Promise<void>

db.beginBulkRemote()

Begins bulk remote operation mode (transaction). Use this for efficient bulk imports.

db.execBulkRemote(sql)

Executes SQL in bulk remote mode.

Parameters:

  • sql (string): SQL statement to execute

db.endBulkRemote(rollback?)

Ends bulk remote mode and commits or rolls back the transaction.

Parameters:

  • rollback (boolean, optional): If true, rollback instead of commit

Returns: Promise<void>

db.getSchemaVersion()

Gets the current schema version.

Returns: number

db.setSchemaVersion(version)

Sets the schema version.

Parameters:

  • version (number): Version number to set

Returns: Promise<void>

db.export()

Exports the database as binary data (for backup/export).

Returns: Uint8Array

db.close()

Closes the database connection.

Returns: Promise<void>

Schema Migrations Example

const db = await SynclibDatabase.open({
  databaseName: 'myapp.db'
});

const currentVersion = db.getSchemaVersion();
const targetVersion = 3;

if (currentVersion < targetVersion) {
  // Run migrations
  if (currentVersion < 1) {
    db.exec(`
      CREATE TABLE users (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT NOT NULL
      )
    `);
    await db.setSchemaVersion(1);
  }

  if (currentVersion < 2) {
    db.exec(`ALTER TABLE users ADD COLUMN created_at INTEGER`);
    await db.setSchemaVersion(2);
  }

  if (currentVersion < 3) {
    db.exec(`
      CREATE TABLE posts (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL,
        title TEXT NOT NULL,
        content TEXT,
        FOREIGN KEY (user_id) REFERENCES users(id)
      )
    `);
    await db.setSchemaVersion(3);
  }
}

Syncing Pattern

Here's a recommended pattern for syncing changes:

// Client side
async function syncWithServer(db: SynclibDatabase) {
  // 1. Get pending local changes
  const localChanges = db.getPendingChanges();

  // 2. Send to server and receive remote changes
  const response = await fetch('/api/sync', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ changes: localChanges })
  });

  const { remoteChanges, lastSeqnum } = await response.json();

  // 3. Apply remote changes
  for (const change of remoteChanges) {
    await db.applyRemote({
      tableName: change.tableName,
      rowId: change.rowId,
      operation: change.operation,
      sql: change.sql, // Server should provide the SQL
      params: change.params,
      data: change.data
    });
  }

  // 4. Mark local changes as synced
  if (localChanges.length > 0) {
    await db.markSynced(lastSeqnum);
  }
}

// Run sync periodically
setInterval(() => syncWithServer(db), 30000); // Every 30 seconds

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors