Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FlutterFlow + Supabase - issue on powersync_db.worker.js script initialization #237

Open
potocnikvid opened this issue Jan 31, 2025 · 1 comment

Comments

@potocnikvid
Copy link

I am having problems with the initialization of PowerSync on my Flutter app build in Flutterflow using Supabase.

I get these errors:

Image Image Image Image Image

This is my frontend init script:

// Automatic FlutterFlow imports
import '/backend/schema/structs/index.dart';
import '/backend/schema/enums/enums.dart';
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'index.dart'; // Imports other custom actions

import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
import 'package:powersync/powersync.dart' as powersync;
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:async';

/**************************************************************
               
POWERSYNC SETUP INSTRUCTIONS 
               
// Paste your PowerSync Client Schema here.
// We recommend generating this from the dashboard using the "Generate client-side schema" action
// See docs https://docs.powersync.com/usage/tools/powersync-dashboard#actions

// NB: You need to prefix all Schema, powersync.Table, powersync.Column and Index calls with "powersync." due to a FF limitation

**************************************************************/

const powersync.Schema schema = powersync.Schema([
  powersync.Table('tbl_job', [
    powersync.Column.text('created_at'),
    powersync.Column.text('created_by'),
    powersync.Column.integer('reference'),
    powersync.Column.text('title'),
    powersync.Column.text('site_id'),
    powersync.Column.text('company_id'),
    powersync.Column.text('contact_id'),
    powersync.Column.text('purchase_order'),
    powersync.Column.real('value')
  ]),
  powersync.Table('tbl_report', [
    powersync.Column.text('created_at'),
    powersync.Column.text('created_by'),
    powersync.Column.text('job_id'),
    powersync.Column.text('report_typet_id'),
    powersync.Column.text('extent_of_survey'),
    powersync.Column.text('hs_info'),
    powersync.Column.text('due_date'),
    powersync.Column.text('filename'),
    powersync.Column.integer('version'),
    powersync.Column.text('version_description'),
    powersync.Column.text('report_url'),
    powersync.Column.text('distribution_list'),
    powersync.Column.text('report_prepared_by'),
    powersync.Column.text('report_approved_by'),
    powersync.Column.integer('task_sitework'),
    powersync.Column.integer('task_lab'),
    powersync.Column.integer('task_plans'),
    powersync.Column.integer('task_approved'),
    powersync.Column.integer('task_issued'),
    powersync.Column.text('site_info'),
    powersync.Column.text('allowances'),
    powersync.Column.text('special_instructions')
  ]),
  powersync.Table('tbl_report_type',
      [powersync.Column.text('title'), powersync.Column.text('app_template')]),
  powersync.Table('tbl_event', [
    powersync.Column.text('created_at'),
    powersync.Column.text('created_by'),
    powersync.Column.text('event_type_id'),
    powersync.Column.text('assigned_to_id'),
    powersync.Column.text('start_datetime'),
    powersync.Column.text('end_datetime'),
    powersync.Column.text('report_id'),
    powersync.Column.text('title'),
    powersync.Column.text('comments'),
    powersync.Column.real('revenue')
  ]),
  powersync.Table('tbl_list', [
    powersync.Column.text('tenant_id'),
    powersync.Column.text('list_name'),
    powersync.Column.text('company_id'),
    powersync.Column.text('created_at')
  ]),
  powersync.Table('tbl_listitem', [
    powersync.Column.text('list_id'),
    powersync.Column.text('text_value'),
    powersync.Column.integer('int_value'),
    powersync.Column.real('float_value'),
    powersync.Column.integer('custom_order'),
    powersync.Column.text('created_at')
  ]),
  powersync.Table('tbl_site', [
    powersync.Column.text('created_at'),
    powersync.Column.text('uprn'),
    powersync.Column.text('site_name'),
    powersync.Column.text('address'),
    powersync.Column.text('postcode'),
    powersync.Column.text('company_id'),
    powersync.Column.text('building_site_id'),
    powersync.Column.integer('asbestos_freq'),
    powersync.Column.text('asb_date'),
    powersync.Column.text('created_by')
  ]),
  powersync.Table('tbl_note', [
    powersync.Column.text('created_at'),
    powersync.Column.text('created_by'),
    powersync.Column.text('title'),
    powersync.Column.text('note'),
    powersync.Column.text('report_id'),
    powersync.Column.integer('archived')
  ]),
  powersync.Table('tbl_attachment', [
    powersync.Column.text('created_at'),
    powersync.Column.text('created_by'),
    powersync.Column.text('type'),
    powersync.Column.text('title'),
    powersync.Column.text('image_urls'),
    powersync.Column.integer('page_number'),
    powersync.Column.text('report_id'),
    powersync.Column.text('paper_size')
  ]),
  powersync.Table('tbl_asbestositem', [
    powersync.Column.text('created_at'),
    powersync.Column.text('created_by'),
    powersync.Column.text('building_id'),
    powersync.Column.integer('register_no'),
    powersync.Column.text('site_id'),
    powersync.Column.text('location_id'),
    powersync.Column.text('item_description'),
    powersync.Column.text('reference'),
    powersync.Column.text('product_type_id'),
    powersync.Column.integer('mra_product_score'),
    powersync.Column.integer('mra_condition_score'),
    powersync.Column.integer('mra_st_score'),
    powersync.Column.integer('mra_analysis_score'),
    powersync.Column.text('sample_id'),
    powersync.Column.text('analysis_result'),
    powersync.Column.integer('pra_required'),
    powersync.Column.integer('pra1'),
    powersync.Column.integer('pra2'),
    powersync.Column.integer('pra3'),
    powersync.Column.integer('pra4'),
    powersync.Column.integer('pra5'),
    powersync.Column.integer('pra6'),
    powersync.Column.integer('pra7'),
    powersync.Column.integer('pra8'),
    powersync.Column.integer('pra9'),
    powersync.Column.integer('pra10'),
    powersync.Column.text('recommendation_id'),
    powersync.Column.text('image_url'),
    powersync.Column.integer('archived'),
    powersync.Column.text('notes'),
    powersync.Column.text('extent'),
    powersync.Column.text('unit'),
    powersync.Column.integer('accessible'),
    powersync.Column.text('approach')
  ]),
  powersync.Table('tbl_asbestoslocation', [
    powersync.Column.text('created_at'),
    powersync.Column.text('building_id'),
    powersync.Column.text('reference'),
    powersync.Column.text('location_name'),
    powersync.Column.integer('accessed'),
    powersync.Column.text('notes'),
    powersync.Column.text('image_url'),
    powersync.Column.text('floor_level'),
    powersync.Column.text('created_by'),
    powersync.Column.text('parent_id'),
    powersync.Column.text('report_id')
  ])
]);

/**************************************************************
               
                       END POWERSYNC SETUP

**************************************************************/

const PowerSyncEndpoint = FFAppConstants.PowerSyncUrl;

late powersync.PowerSyncDatabase db;

//create one of these for each of your watch() queries
StreamSubscription listsSubscription = Stream<void>.empty().listen((event) {});

const bool kIsWeb = bool.fromEnvironment('dart.library.js_util');

/// Postgres Response codes that we cannot recover from by retrying.
final List<RegExp> fatalResponseCodes = [
  // Class 22 — Data Exception
  // Examples include data type mismatch.
  RegExp(r'^22...$'),
  // Class 23 — Integrity Constraint Violation.
  // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations.
  RegExp(r'^23...$'),
  // INSUFFICIENT PRIVILEGE - typically a row-level security violation
  RegExp(r'^42501$'),
];

class SupabaseConnector extends powersync.PowerSyncBackendConnector {
  powersync.PowerSyncDatabase db;
  Future<void>? _refreshFuture;

  SupabaseConnector(this.db);

  /// Get a Supabase token to authenticate against the PowerSync instance.
  @override
  Future<powersync.PowerSyncCredentials?> fetchCredentials() async {
    // Wait for pending session refresh if any
    await _refreshFuture;

    // Use Supabase token for PowerSync
    final session = Supabase.instance.client.auth.currentSession;
    if (session == null) {
      // Not logged in
      return null;
    }
    // Use the access token to authenticate against PowerSync
    final token = session.accessToken;

    // userId and expiresAt are for debugging purposes only
    final userId = session.user.id;
    final expiresAt = session.expiresAt == null
        ? null
        : DateTime.fromMillisecondsSinceEpoch(session.expiresAt! * 1000);
    return powersync.PowerSyncCredentials(
        endpoint: PowerSyncEndpoint,
        token: token,
        userId: userId,
        expiresAt: expiresAt);
  }

  @override
  void invalidateCredentials() {
    // Trigger a session refresh if auth fails on PowerSync.
    // Generally, sessions should be refreshed automatically by Supabase.
    // However, in some cases it can be a while before the session refresh is
    // retried. We attempt to trigger the refresh as soon as we get an auth
    // failure on PowerSync.
    //
    // This could happen if the device was offline for a while and the session
    // expired, and nothing else attempt to use the session it in the meantime.
    //
    // Timeout the refresh call to avoid waiting for long retries,
    // and ignore any errors. Errors will surface as expired tokens.
    _refreshFuture = Supabase.instance.client.auth
        .refreshSession()
        .timeout(const Duration(seconds: 5))
        .then((response) => null, onError: (error) => null);
  }

  // Upload pending changes to Supabase.
  @override
  Future<void> uploadData(powersync.PowerSyncDatabase database) async {
    // This function is called whenever there is data to upload, whether the
    // device is online or offline.
    // If this call throws an error, it is retried periodically.
    final transaction = await database.getNextCrudTransaction();
    if (transaction == null) {
      return;
    }

    final rest = Supabase.instance.client.rest;
    powersync.CrudEntry? lastOp;
    try {
      // Note: If transactional consistency is important, use database functions
      // or edge functions to process the entire transaction in a single call.
      for (var op in transaction.crud) {
        lastOp = op;

        final table = rest.from(op.table);
        if (op.op == powersync.UpdateType.put) {
          var data = Map<String, dynamic>.of(op.opData!);
          data['id'] = op.id;
          await table.upsert(data);
        } else if (op.op == powersync.UpdateType.patch) {
          await table.update(op.opData!).eq('id', op.id);
        } else if (op.op == powersync.UpdateType.delete) {
          await table.delete().eq('id', op.id);
        }
      }

      // All operations successful.
      await transaction.complete();
    } on PostgrestException catch (e) {
      if (e.code != null &&
          fatalResponseCodes.any((re) => re.hasMatch(e.code!))) {
        /// Instead of blocking the queue with these errors,
        /// discard the (rest of the) transaction.
        ///
        /// Note that these errors typically indicate a bug in the application.
        /// If protecting against data loss is important, save the failing records
        /// elsewhere instead of discarding, and/or notify the user.
        print('Data upload error - discarding $lastOp' + e.toString());
        await transaction.complete();
      } else {
        // Error may be retryable - e.g. network error or temporary server error.
        // Throwing an error here causes this call to be retried after a delay.
        rethrow;
      }
    }
  }
}

bool isLoggedIn() {
  return Supabase.instance.client.auth.currentSession?.accessToken != null;
}

Future initpowersync() async {
  db = powersync.PowerSyncDatabase(
      schema: schema, path: await getDatabasePath());

  await db.initialize();

  SupabaseConnector? currentConnector;

  if (isLoggedIn()) {
    // If the user is already logged in, connect immediately.
    // Otherwise, connect once logged in.
    currentConnector = SupabaseConnector(db);
    db.connect(connector: currentConnector);
  }

  Supabase.instance.client.auth.onAuthStateChange.listen((data) async {
    final AuthChangeEvent event = data.event;
    if (event == AuthChangeEvent.signedIn) {
      // Connect to PowerSync when the user is signed in
      currentConnector = SupabaseConnector(db);
      db.connect(connector: currentConnector!);
    } else if (event == AuthChangeEvent.signedOut) {
      // Implicit sign out - disconnect, but don't delete data
      currentConnector = null;
      await db.disconnect();
    } else if (event == AuthChangeEvent.tokenRefreshed) {
      // Supabase token refreshed - trigger token refresh for PowerSync.
      currentConnector?.prefetchCredentials();
    }
  });

  return;
}

Future<String> getDatabasePath() async {
  var path = 'powersync-sqlite.db';
  // getApplicationSupportDirectory is not supported on Web
  if (!kIsWeb) {
    final dir = await getApplicationSupportDirectory();
    path = join(dir.path, path);
  }
  return path;
}

There are no logs in the dashboards so I am assuming this must be something on the SDK side.
Thank you for your help.

@rkistner
Copy link
Contributor

The text/html content-type gives an indication that the file does not exist at the expected path in your project.

There is a command you need to run to download the worker js and WASM files, documented here: https://docs.powersync.com/client-sdk-references/flutter/flutter-web-support#additional-config

We are also actively working on removing that requirement for development, see #233.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants