PowerSync JS Demos

repository·main·Indexed 20 days ago

https://github.com/powersync-ja/powersync-js

A collection of implementation examples for the PowerSync sync engine, featuring integrations with Angular, Supabase, Capacitor (iOS/Android), Electron (Main and Renderer processes), and Next.js. Includes guides on configuring workers, JWT authentication, and local-first development using SQLite and server-side databases like Postgres or MySQL.

Tokens
80.7K
Snippets
267
Records
355
Agent score
71%

What's inside powersync-js

  1. What is PowerSync?

    main
    PowerSync is a service and a set of SDKs designed to help developers build offline-first, real-time, reactive applications. It works by keeping a Postgres database in sync with an on-device SQLite database, ensuring that the application can function seamlessly even with poor or no connectivity.
  2. Overview of PowerSync JavaScript SDKs

    main

    PowerSync is a sync engine designed for building local-first applications. It enables an instantly-responsive UI/UX by syncing data between a client-side SQLite database and a server-side database (such as Postgres, MongoDB, MySQL, or SQL Server).

    The powersync-js monorepo provides SDKs for various environments including:

    • React Native: Mobile implementation.
    • Web: JavaScript Web SDK.
    • Node.js: Server-side/CLI implementation.
    • Framework Integrations: React, Vue, and TanStack Query (React Query).
    • ORM Drivers: Kysely and Drizzle integrations.
    • Attachments: Utilities for handling file attachments.
  3. Project Structure of the Nuxt Supabase Demo

    main

    The following directory structure outlines the key files in this demo:

    • powersync/: Contains AppSchema.ts (schema definition) and SuperbaseConnector.ts (Supabase connector implementation).
    • plugins/powersync.client.ts: PowerSync client-side plugin setup.
    • pages/: Application routes (index.vue, login.vue, confirm.vue).
    • components/AppHeader.vue: UI components.
    • db/seed.sql: Database initialization scripts.
    • powersync.yaml: PowerSync server configuration.
    • sync-config.yaml: PowerSync sync stream definitions.
    • nuxt.config.ts: Nuxt framework configuration.
  4. Use the PowerSync SQL-JS Adapter for development

    main

    The @powersync/adapter-sql-js package provides a pure JavaScript SQLite implementation using SQL.js. It is designed to streamline the development workflow by eliminating the need for native dependencies, making it ideal for environments like Expo Go or other JavaScript-only runtimes.

    ⚠️ Important Limitations

    • Performance: This adapter is significantly slower than native adapters. Every write operation triggers a complete rewrite of the entire database file to persistent storage.
    • Reliability: It does not provide SQLite consistency guarantees. If the application is killed during a write operation, you may experience data loss or database corruption.
    • Production Recommendation: For production React Native apps, use the default adapter in @powersync/react-native (based on OP-SQLite) for substantially better performance and reliability.
  5. Configure a DatabaseSource

    main

    The DatabaseSource type defines how a PowerSync database can be initialized. It supports three modes:

    1. Opened: Providing an existing DBAdapter instance.
    2. Factory: Providing a SQLOpenFactory to create the adapter.
    3. Database: Providing OpenOptions (configuration) to initialize the database.
    // Example of the three shapes of DatabaseSource:
    
    // 1. Using an existing adapter
    const source1: DatabaseSource = { opened: myDbAdapter };
    
    // 2. Using a factory
    const source2: DatabaseSource = { factory: mySqlOpenFactory };
    
    // 3. Using configuration options
    const source3: DatabaseSource = { database: { path: 'my-db.sqlite' } };
  6. Prevent Suspense fallback when updating query parameters

    main

    When you change the parameters of a useSuspenseQuery (e.g., changing a filter or a limit), the hook restarts and enters a suspending state, which triggers the <Suspense> fallback.

    To keep displaying the current (stale) data while the new query is fetching, you should wrap the parameter update in React's startTransition or use the useDeferredValue hook. This prevents the UI from jumping back to the loading state.

    // Example using startTransition to prevent fallback
    import { ErrorBoundary } from 'react-error-boundary';
    import React, { Suspense } from 'react';
    import { useSuspenseQuery } from '@powersync/react';
    
    const TodoListContent = () => {
      const [query, setQuery] = React.useState('SELECT * FROM lists');
      const { data: todoLists } = useSuspenseQuery(query);
    
      return (
        <div
          >
          <button
            onClick={() => {
              React.startTransition(() => setQuery('SELECT * from lists limit 1'));
            }}>
            Update
          </button>
          <ul
            >{todoLists.map((list) => (
              <li key={list.id}>{list.name}</li
            ))}
          </ul
        </div
      );
    };
    
    export const TodoListDisplaySuspense = () => {
      return (
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        <Suspense fallback={<div>Loading todo lists...</div>}>
          <TodoListContent />
        </Suspense>
      </ErrorBoundary>
      );
    };
  7. How Optional Sync works using local-only tables and viewName

    main

    The demo implements a pattern to allow users to use the app without an account, then seamlessly sync their data once they register.

    Core Concept: Local-only Tables

    The app uses local-only tables to persist data before registration. These tables do not log updates in the upload queue, preventing unnecessary database growth.

    To avoid changing application queries when a user signs in, the app uses the viewName property to override table names.

    1. Local-only State: Local tables (e.g., local_lists) have their viewName set to the standard table name (e.g., lists). The app queries SELECT * FROM lists, which points to the local table. The actual synced tables are assigned an inactive_synced_ view name so they are ignored.
    2. Transition: When the user signs in, updateSchema is called to change the viewName of the synced tables back to the standard names (e.g., lists).
    3. Data Migration: Data is copied from the local-only tables to the synced tables, and the local-only data is deleted to save space. The synced tables then automatically begin uploading data to the backend.

    Limitations

    • updateSchema cannot be called inside a transaction. It is recommended to perform schema updates when the database is not connected.
  8. Platform support for PowerSync Capacitor SDK

    main

    The @powersync/capacitor SDK supports the following platforms with different underlying drivers:

    PlatformDriver Implementation
    AndroidNative SQLite via Capacitor Community SQLite
    iOSNative SQLite via Capacitor Community SQLite
    WebWASQLite via PowerSync Web SDK
    ElectronWASQLite via PowerSync Web SDK
  9. Use Incremental Queries to optimize data references

    main

    By default, useQuery triggers a new data array reference whenever any change is detected in the underlying tables, even if the specific query result hasn't changed. This can cause frequent re-renders.

    Incremental Queries solve this by performing in-memory comparisons. When configured with a rowComparator, the data array reference remains the same if the result set is unchanged. Additionally, individual row object references are preserved for unchanged rows.

    To enable this, provide a rowComparator object in the options:

    • keyBy: A function to extract a unique key from an item.
    • compareBy: A function to determine if an item has changed (e.g., using JSON.stringify).
    const { data, isLoading, isFetching } = useQuery(
      `SELECT * FROM cats WHERE breed = 'tabby'`, 
      [], 
      {
        rowComparator: {
          keyBy: (item) => item.id,
          compareBy: (item) => JSON.stringify(item)
        }
      }
    )
  10. Implement the watchAttachments callback

    main

    The watchAttachments callback is a required configuration for AttachmentQueue. It tells the queue which attachments it is responsible for (downloading, uploading, or archiving) by providing a list of WatchedAttachmentItem objects.

    Important: Each item in the array must provide either fileExtension OR filename, but not both.

    WatchedAttachmentItem Types:

    • { id: string; fileExtension: string; metaData?: string; mediaType?: string; }
    • { id: string; filename: string; metaData?: string; mediaType?: string; }

    Example Implementation: Use a PowerSync db.watch() query to detect changes in your application tables (e.g., a users table containing a photo_id) and map those rows to WatchedAttachmentItem objects.

    watchAttachments: (onUpdate) => {
      // Watch photo references in users table
      db.watch(
        'SELECT photo_id, metadata FROM users WHERE photo_id IS NOT NULL',
        [],
        {
          onResult: async (result) => {
            const attachments = result.rows?._array.map(row => ({
              id: row.photo_id,
              fileExtension: 'jpg',
              metaData: row.metadata
            })) ?? [];
            await onUpdate(attachments);
          }
        }
      );
    }