production-taskbar-client / src / renderer / apis / cache.js
cache.js
Raw
import log from "electron-log/renderer";

import getDatabase from "../app/local_storage/rxdb";
import { APP_SETTINGS } from "../app/local_storage/schemas";
import { cacheDataUrl } from "./cacheSlice";

export const IconTag = "icon"; // property name in json result that that should be cached as attachment

let dispatch;
export const setBackendDispatch = (storeDispatch) => {
  dispatch = storeDispatch;
};

const toBlobUrl = async (url) => {
  let blob;
  const response = await fetch(url);
  if (response.ok) blob = await response.blob();
  return blob;
};

const blobToBase64 = async (blob) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = (err) => reject(err);
    reader.readAsDataURL(blob);
  });
};

const putUrlAsAttachment = async (url, db, name) => {
  const blob = await toBlobUrl(url);
  if (blob && document) {
    const dataURL = await blobToBase64(blob);
    const result = await db.attachments.upsert({
      name: name.substring(0, 40), // maxLength in schema
      path: url.pathname,
      type: blob.type,
      base64data: dataURL,
    });
    return result;
  }
  return null;
};

// Parse attachments from response and extract all attachments with IconTag into {name: path, ...}
const parseAttachments = (object, settingPropName) => {
  const result = {};

  const recursiveSearch = (o, name) => {
    if (o) {
      Object.entries(o).forEach(([k, v]) => {
        if (v && typeof v === "object") recursiveSearch(v, v.name ?? v.title);
        if (v && k === IconTag)
          result[`${settingPropName}_${name}_${IconTag}`] = v;
      });
    }
  };

  recursiveSearch(object);
  return result;
};

const cacheAttachments = async (object, db, settingPropName) => {
  const attachments = parseAttachments(object, settingPropName);
  const cachedAttachments = await db.attachments.find().exec();
  Object.entries(attachments).forEach(async ([name, path]) => {
    // in memory previous data check to prevent refetching
    const cachedAttachment = cachedAttachments.find(
      (a) => a.get("name", false) === name
    );
    if (cachedAttachment) {
      const previousPath = cachedAttachment.get("path");
      if (path === previousPath) {
        return;
      } // stop if attachment has not changed
    }
    // continue and refetch/cache attachment
    const url = new URL(path, process.env.BACKEND_URL);
    const attachment = await putUrlAsAttachment(url, db, name); // caching to rxdb as document
    if (attachment) {
      const dataURL = attachment.get("base64data");
      dispatch(cacheDataUrl({ name, dataURL })); // caching to store
    }
  });
};

export const clearWorkplace = () =>
  getDatabase()
    .then((db) => db.settings.findOne(APP_SETTINGS).exec())
    .then((document) => {
      document?.remove();
      return true;
    });

export const cache = (settingPropName, response, storeAction) =>
  getDatabase()
    .then((db) =>
      Promise.all([
        Promise.resolve(db),
        db.settings.findOne(APP_SETTINGS).exec(),
      ])
    )
    .then(async (value) => {
      const [db, setting] = value;

      // caching to rxdb
      if (setting) {
        await setting.incrementalModify((oldData) => {
          oldData[settingPropName] = response;
          return oldData;
        });
      } else {
        await db.settings.upsert({
          _id: APP_SETTINGS,
          [settingPropName]: response,
        });
      }

      await cacheAttachments(response, db, settingPropName);
      dispatch(storeAction(response)); // caching to store
      return true;
    })
    .catch((e) => {
      log.error(
        `Caching error in ${[settingPropName]}: ${JSON.stringify(e).substring(
          0,
          300
        )}...`
      );
    });