cyb/src/contexts/backend/backend.tsx

import { proxy, Remote } from 'comlink';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { selectCurrentAddress } from 'src/redux/features/pocket';
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import { RESET_SYNC_STATE_ACTION_NAME } from 'src/redux/reducers/backend';
import RxBroadcastChannelListener from 'src/services/backend/channels/RxBroadcastChannelListener';
import DbApiWrapper from 'src/services/backend/services/DbApi/DbApi';
import { backgroundWorkerInstance } from 'src/services/backend/workers/background/service';
import { BackgroundWorker } from 'src/services/backend/workers/background/worker';
import { cozoDbWorkerInstance } from 'src/services/backend/workers/db/service';
import { CozoDbWorker } from 'src/services/backend/workers/db/worker';

import { DB_NAME } from 'src/services/CozoDb/cozoDb';
import { getIpfsOpts } from 'src/services/ipfs/config';
// import BroadcastChannelListener from 'src/services/backend/channels/BroadcastChannelListener';

import { Observable } from 'rxjs';
import { EmbeddingApi } from 'src/services/backend/workers/background/api/mlApi';
import { RuneEngine } from 'src/services/scripting/engine';
import { Option } from 'src/types';
import { createSenseApi, SenseApi } from './services/senseApi';

const setupStoragePersistence = async () => {
  let isPersistedStorage: boolean;
  try {
    isPersistedStorage = await navigator.storage.persisted();
    if (!isPersistedStorage) {
      await navigator.permissions.query({ name: 'persistent-storage' });
      isPersistedStorage = true;
    }
  } catch (error) {
    console.log('[Backend] failed to get persistence status', error);
    isPersistedStorage = false;
  }
  const message = isPersistedStorage ? `๐Ÿ”ฐ storage is persistent` : `โš ๏ธ storage is non-persitent`;

  console.log(message);

  return isPersistedStorage;
};

type BackendProviderContextType = {
  cozoDbRemote: Remote<CozoDbWorker> | null;
  senseApi: SenseApi;
  ipfsApi: Remote<BackgroundWorker['ipfsApi']> | null;
  dbApi: DbApiWrapper | null;
  ipfsError?: string | null;
  isIpfsInitialized: boolean;
  isDbInitialized: boolean;
  isSyncInitialized: boolean;
  isReady: boolean;
  embeddingApi$: Promise<Observable<EmbeddingApi>>;
  rune: Remote<RuneEngine>;
};

const valueContext = {
  cozoDbRemote: null,
  senseApi: null,
  isIpfsInitialized: false,
  isDbInitialized: false,
  isSyncInitialized: false,
  isReady: false,
  dbApi: null,
  ipfsApi: null,
};

const BackendContext = React.createContext<BackendProviderContextType>(valueContext);

export function useBackend() {
  return useContext(BackendContext);
}

window.cyb.db = {
  clear: () => indexedDB.deleteDatabase(DB_NAME),
};

const isMobileTauri =
  !!process.env.IS_TAURI &&
  /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

function BackendProvider({ children }: { children: React.ReactNode }) {
  const dispatch = useAppDispatch();
  // const { defaultAccount } = useAppSelector((state) => state.pocket);

  const [ipfsError, setIpfsError] = useState(null);

  const isDbInitialized = useAppSelector((state) => state.backend.services.db.status === 'started');
  const isIpfsInitialized = useAppSelector(
    (state) => state.backend.services.ipfs.status === 'started'
  );
  const isSyncInitialized = useAppSelector(
    (state) => state.backend.services.sync.status === 'started'
  );
  const [needPFSInitialize, setNeedPFSInitialize] = useState(
    !!process.env.IS_TAURI && !isMobileTauri
  );

  const myAddress = useAppSelector(selectCurrentAddress);

  const { friends, following } = useAppSelector((state) => state.backend.community);

  // // TODO: preload from DB
  const followings = useMemo(() => {
    return Array.from(new Set([...friends, ...following]));
  }, [friends, following]);

  // On mobile Tauri, skip heavy backend requirements for isReady
  const isReady = isMobileTauri
    ? true
    : isDbInitialized &&
      isIpfsInitialized &&
      isSyncInitialized &&
      !needPFSInitialize;
  const [_embeddingApi$, setEmbeddingApi] = useState<Option<Observable<EmbeddingApi>>>(undefined);
  // const embeddingApiRef = useRef<Observable<EmbeddingApi>>();
  useEffect(() => {
    // On mobile Tauri, start only IPFS (remote gateway) โ€” skip CozoDb, ML, Rune, sync
    if (isMobileTauri) {
      console.log('[Backend] Mobile Tauri โ€” starting IPFS with remote gateway');

      // Broadcast channel receives service status updates from background worker
      const channel = new RxBroadcastChannelListener(dispatch);

      // Connect to remote IPFS gateway (io.cybernode.ai)
      backgroundWorkerInstance.ipfsApi
        .start(getIpfsOpts())
        .then(() => {
          setIpfsError(null);
          console.log('[Backend] Mobile IPFS connected');
        })
        .catch((err) => {
          setIpfsError(err);
          console.error('[Backend] Mobile IPFS failed:', err);
        });

      return;
    }

    console.log(
      process.env.IS_DEV
        ? '๐Ÿงช Starting backend in DEV mode...'
        : '๐Ÿงฌ Starting backend in PROD mode...'
    );

    (async () => {
      // embeddingApiRef.current = await backgroundWorkerInstance.embeddingApi$;
      const embeddingApiInstance$ = await backgroundWorkerInstance.embeddingApi$;
      setEmbeddingApi(embeddingApiInstance$);
    })();

    setupStoragePersistence();

    const _channel = new RxBroadcastChannelListener(dispatch);

    const startIpfsWithRetry = async (retries = 3, delays = [5000, 15000, 45000]) => {
      for (let attempt = 0; attempt <= retries; attempt++) {
        try {
          const ipfsOpts = getIpfsOpts();
          // eslint-disable-next-line no-await-in-loop
          await backgroundWorkerInstance.ipfsApi.start(ipfsOpts);
          setIpfsError(null);
          if (attempt > 0) {
            console.log(`๐Ÿ”ฐ IPFS connected after ${attempt + 1} attempts`);
          }
          return;
        } catch (err) {
          if (attempt < retries) {
            const delay = delays[attempt] || delays[delays.length - 1];
            console.warn(
              `โš ๏ธ IPFS connection failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${delay / 1000}s...`
            );
            // eslint-disable-next-line no-await-in-loop
            await new Promise((resolve) => {
              setTimeout(resolve, delay);
            });
          } else {
            setIpfsError(err);
            console.error(`โ˜ ๏ธ IPFS failed after ${retries + 1} attempts: ${err}`);
          }
        }
      }
    };

    // In Tauri mode, IPFS connection is handled by the Tauri useEffect below
    // which waits for the daemon to be ready before connecting
    if (!process.env.IS_TAURI) {
      startIpfsWithRetry();
    }

    cozoDbWorkerInstance.init().then(() => {
      // const dbApi = createDbApi();
      const dbApi = new DbApiWrapper();

      dbApi.init(proxy(cozoDbWorkerInstance));
      setDbApi(dbApi);
      // pass dbApi into background worker
      return backgroundWorkerInstance.injectDb(proxy(dbApi));
    });
  }, [dispatch]);

  useEffect(() => {
    if (isMobileTauri) return;
    backgroundWorkerInstance.setParams({ myAddress });
    dispatch({ type: RESET_SYNC_STATE_ACTION_NAME });
  }, [myAddress, dispatch]);

  useEffect(() => {
    isReady && console.log('๐ŸŸข backend started!');
  }, [isReady]);

  useEffect(() => {
    if (process.env.IS_TAURI && !isMobileTauri) {
      console.log('[Backend] need initialize IPFS for TAURI env');

      const connectIpfsClient = async (): Promise<boolean> => {
        const retries = 5;
        const delays = [2000, 5000, 10000, 15000, 30000];
        for (let attempt = 0; attempt < retries; attempt++) {
          try {
            // eslint-disable-next-line no-await-in-loop
            await backgroundWorkerInstance.ipfsApi.start(getIpfsOpts());
            setIpfsError(null);
            console.log(
              attempt > 0
                ? `[Backend] IPFS client connected after ${attempt + 1} attempts`
                : '[Backend] IPFS client connected'
            );
            return true;
          } catch (err) {
            if (attempt < retries - 1) {
              const delay = delays[attempt] || delays[delays.length - 1];
              console.warn(
                `[Backend] IPFS client connect failed (attempt ${attempt + 1}/${retries}), retrying in ${delay / 1000}s...`,
                err
              );
              // eslint-disable-next-line no-await-in-loop
              await new Promise((r) => setTimeout(r, delay));
            } else {
              setIpfsError(err);
              console.error(`[Backend] IPFS client failed after ${retries} attempts:`, err);
            }
          }
        }
        return false;
      };

      (async () => {
        try {
          // Poll until IPFS daemon is running (started by Rust side)
          const maxAttempts = 30;
          for (let i = 0; i < maxAttempts; i++) {
            try {
              const response = await fetch('http://127.0.0.1:5001/api/v0/id', {
                method: 'POST',
              });
              if (response.ok) {
                console.log('[Backend] IPFS API is reachable');
                break;
              }
            } catch {
              // not ready yet
            }
            console.log(`[Backend] Waiting for IPFS API... (${i + 1}/${maxAttempts})`);
            await new Promise((r) => setTimeout(r, 2000));
          }

          setNeedPFSInitialize(false);
          console.log('[Backend] IPFS is ready for TAURI');

          const connected = await connectIpfsClient();

          // If initial connection failed, keep retrying every 30s
          if (!connected) {
            const retryInterval = setInterval(async () => {
              console.log('[Backend] Retrying IPFS connection...');
              const ok = await connectIpfsClient();
              if (ok) clearInterval(retryInterval);
            }, 30_000);
          }
        } catch (error) {
          console.error('Failed to initialize IPFS for Tauri', error);
          setNeedPFSInitialize(false);
        }
      })();
    }
  }, []);

  const [dbApi, setDbApi] = useState<DbApiWrapper | null>(null);

  const senseApi = useMemo(() => {
    if (isDbInitialized && dbApi && myAddress) {
      return createSenseApi(dbApi, myAddress, followings);
    }
    return null;
  }, [isDbInitialized, dbApi, myAddress, followings]);

  useEffect(() => {
    if (isMobileTauri) return;
    (async () => {
      backgroundWorkerInstance.setRuneDeps({
        address: myAddress,
        // TODO: proxify particular methods
        // senseApi: senseApi ? proxy(senseApi) : undefined,
        // signingClient: signingClient ? proxy(signingClient) : undefined,
      });
    })();
  }, [myAddress]);

  const ipfsApi = useMemo(() => backgroundWorkerInstance.ipfsApi, []);

  const valueMemo = useMemo(
    () =>
      ({
        rune: backgroundWorkerInstance.rune,
        embeddingApi$: backgroundWorkerInstance.embeddingApi$,
        cozoDbRemote: cozoDbWorkerInstance,
        ipfsApi,
        dbApi,
        senseApi,
        ipfsError,
        isIpfsInitialized,
        isDbInitialized,
        isSyncInitialized,
        isReady,
      }) as BackendProviderContextType,
    [
      isReady,
      isIpfsInitialized,
      isDbInitialized,
      isSyncInitialized,
      ipfsError,
      senseApi,
      dbApi,
      ipfsApi,
    ]
  );

  return <BackendContext.Provider value={valueMemo}>{children}</BackendContext.Provider>;
}

export default BackendProvider;

Synonyms

pussy-ts/src/contexts/backend/backend.tsx

Neighbours