Compare commits

...

3 Commits

Author SHA1 Message Date
Satish Gandham
733b725b5e Extract utility functions out of Storage class 2024-12-24 16:17:00 +05:30
Satish Gandham
3bee051ee9 Fix typos and add comments 2024-12-24 14:32:53 +05:30
Satish Gandham
6b902d458e Add comments to stroage class 2024-12-18 16:00:17 +05:30
6 changed files with 267 additions and 119 deletions

View File

@@ -3,37 +3,49 @@ import * as Comlink from "comlink";
import set from "lodash/set";
// plane
import { EIssueGroupBYServerToProperty } from "@plane/constants";
import { TIssue } from "@plane/types";
// lib
import { rootStore } from "@/lib/store-context";
// services
import { IssueService } from "@/services/issue/issue.service";
//
import { ARRAY_FIELDS, BOOLEAN_FIELDS } from "./utils/constants";
import { getSubIssuesWithDistribution } from "./utils/data.utils";
import createIndexes from "./utils/indexes";
import { addIssuesBulk, syncDeletesToLocal } from "./utils/load-issues";
import { loadWorkSpaceData } from "./utils/load-workspace";
import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor";
import { runQuery } from "./utils/query-executor";
import { deleteOption, formatLocalIssue, getLastSyncTime, getOption, setOption } from "./utils/storage.sqlite.utils";
import { createTables } from "./utils/tables";
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
/** Database version for schema management */
const DB_VERSION = 1;
/** Number of items per page for pagination */
const PAGE_SIZE = 500;
/** Number of items to process in a single batch */
const BATCH_SIZE = 50;
/** Project status type definition */
type TProjectStatus = {
issues: { status: undefined | "loading" | "ready" | "error" | "syncing"; sync: Promise<void> | undefined };
issues: {
status: undefined | "loading" | "ready" | "error" | "syncing";
sync: Promise<void> | undefined;
};
};
/** Database status type definition */
type TDBStatus = "initializing" | "ready" | "error" | undefined;
/**
* Storage class for managing local SQLite database operations
* Handles database initialization, synchronization, and CRUD operations for issues
*/
export class Storage {
db: any;
status: TDBStatus = undefined;
dbName = "plane";
projectStatus: Record<string, TProjectStatus> = {};
workspaceSlug: string = "";
public db: any;
private status: TDBStatus = undefined;
private dbName = "plane";
private projectStatus: Record<string, TProjectStatus> = {};
private workspaceSlug: string = "";
constructor() {
this.db = null;
@@ -43,13 +55,20 @@ export class Storage {
}
}
closeDBConnection = async () => {
/**
* Closes the database connection
* @returns Promise<void>
*/
private closeDBConnection = async (): Promise<void> => {
if (this.db) {
await this.db.close();
}
};
reset = () => {
/**
* Resets the database state
*/
public reset = (): void => {
if (this.db) {
this.db.close();
}
@@ -59,17 +78,27 @@ export class Storage {
this.workspaceSlug = "";
};
clearStorage = async (force = false) => {
/**
* Clears the storage and resets the database
* @param force - Force clear storage
*/
public clearStorage = async (force = false): Promise<void> => {
try {
await this.db?.close();
await clearOPFS(force);
this.reset();
} catch (e) {
console.error("Error clearing sqlite sync storage", e);
} catch (error) {
logError(error);
console.error("Error clearing sqlite sync storage", error);
}
};
initialize = async (workspaceSlug: string): Promise<boolean> => {
/**
* Initializes the database for a given workspace
* @param workspaceSlug - Workspace identifier
* @returns Promise<boolean> - True if initialization is successful
*/
public initialize = async (workspaceSlug: string): Promise<boolean> => {
if (!rootStore.user.localDBEnabled) return false; // return if the window gets hidden
if (workspaceSlug !== this.workspaceSlug) {
@@ -86,7 +115,12 @@ export class Storage {
}
};
_initialize = async (workspaceSlug: string): Promise<boolean> => {
/**
* Initializes the database for a given workspace
* @param workspaceSlug - Workspace identifier
* @returns Promise<boolean> - True if initialization is successful
*/
private _initialize = async (workspaceSlug: string): Promise<boolean> => {
if (this.status === "initializing") {
console.warn(`Initialization already in progress for workspace ${workspaceSlug}`);
return false;
@@ -122,7 +156,7 @@ export class Storage {
// Your SQLite code here.
await createTables();
await this.setOption("DB_VERSION", DB_VERSION.toString());
await setOption("DB_VERSION", DB_VERSION.toString());
return true;
} catch (error) {
this.status = "error";
@@ -131,7 +165,10 @@ export class Storage {
}
};
syncWorkspace = async () => {
/**
* Synchronizes the workspace data
*/
public syncWorkspace = async (): Promise<void> => {
if (!rootStore.user.localDBEnabled) return;
const syncInProgress = await this.getIsWriteInProgress("sync_workspace");
if (syncInProgress) {
@@ -140,23 +177,27 @@ export class Storage {
}
try {
await startSpan({ name: "LOAD_WS", attributes: { slug: this.workspaceSlug } }, async () => {
this.setOption("sync_workspace", new Date().toUTCString());
setOption("sync_workspace", new Date().toUTCString());
await loadWorkSpaceData(this.workspaceSlug);
this.deleteOption("sync_workspace");
deleteOption("sync_workspace");
});
} catch (e) {
logError(e);
this.deleteOption("sync_workspace");
deleteOption("sync_workspace");
}
};
syncProject = async (projectId: string) => {
/**
* Synchronizes the project data
* @param projectId - Project identifier
*/
public syncProject = async (projectId: string): Promise<void> => {
if (
// document.hidden ||
!rootStore.user.localDBEnabled
)
return false; // return if the window gets hidden
) {
return; // return if the window gets hidden
}
// Load labels, members, states, modules, cycles
await this.syncIssues(projectId);
@@ -172,9 +213,13 @@ export class Storage {
// this.setOption("workspace_synced_at", new Date().toISOString());
};
syncIssues = async (projectId: string) => {
/**
* Synchronizes the issues for a project
* @param projectId - Project identifier
*/
public syncIssues = async (projectId: string): Promise<void> => {
if (!rootStore.user.localDBEnabled || !this.db) {
return false;
return;
}
try {
const sync = startSpan({ name: `SYNC_ISSUES` }, () => this._syncIssues(projectId));
@@ -186,7 +231,11 @@ export class Storage {
}
};
_syncIssues = async (projectId: string) => {
/**
* Synchronizes the issues for a project
* @param projectId - Project identifier
*/
private _syncIssues = async (projectId: string): Promise<void> => {
const activeSpan = getActiveSpan();
log("### Sync started");
@@ -207,8 +256,8 @@ export class Storage {
description: true,
};
const syncedAt = await this.getLastSyncTime(projectId);
const projectSync = await this.getOption(projectId);
const syncedAt = await getLastSyncTime(projectId);
const projectSync = await getOption(projectId);
if (syncedAt) {
queryParams["updated_at__gt"] = syncedAt;
@@ -245,7 +294,7 @@ export class Storage {
if (status === "loading") {
await createIndexes();
}
this.setOption(projectId, "ready");
setOption(projectId, "ready");
this.setStatus(projectId, "ready");
this.setSync(projectId, undefined);
@@ -255,31 +304,15 @@ export class Storage {
});
};
getIssueCount = async (projectId: string) => {
const count = await runQuery(`select count(*) as count from issues where project_id='${projectId}'`);
return count[0]["count"];
};
getLastUpdatedIssue = async (projectId: string) => {
const lastUpdatedIssue = await runQuery(
`select id, name, updated_at , sequence_id from issues WHERE project_id='${projectId}' AND is_local_update IS NULL order by datetime(updated_at) desc limit 1 `
);
if (lastUpdatedIssue.length) {
return lastUpdatedIssue[0];
}
return;
};
getLastSyncTime = async (projectId: string) => {
const issue = await this.getLastUpdatedIssue(projectId);
if (!issue) {
return false;
}
return issue.updated_at;
};
getIssues = async (workspaceSlug: string, projectId: string, queries: any, config: any) => {
/**
* Gets issues for a project
* @param workspaceSlug - Workspace identifier
* @param projectId - Project identifier
* @param queries - Query parameters
* @param config - Configuration options
* @returns Promise<any> - Issues data
*/
public getIssues = async (workspaceSlug: string, projectId: string, queries: any, config: any): Promise<any> => {
log("#### Queries", queries);
const currentProjectStatus = this.getStatus(projectId);
@@ -387,7 +420,12 @@ export class Storage {
return out;
};
getIssue = async (issueId: string) => {
/**
* Gets an issue by ID
* @param issueId - Issue identifier
* @returns Promise<any | undefined> - Issue data or undefined
*/
public getIssue = async (issueId: string): Promise<any | undefined> => {
try {
if (!rootStore.user.localDBEnabled || this.status !== "ready") return;
@@ -403,8 +441,15 @@ export class Storage {
return;
};
getSubIssues = async (workspaceSlug: string, projectId: string, issueId: string) => {
const workspace_synced_at = await this.getOption("workspace_synced_at");
/**
* Gets sub-issues for an issue
* @param workspaceSlug - Workspace identifier
* @param projectId - Project identifier
* @param issueId - Issue identifier
* @returns Promise<any> - Sub-issues data
*/
public getSubIssues = async (workspaceSlug: string, projectId: string, issueId: string): Promise<any> => {
const workspace_synced_at = await getOption("workspace_synced_at");
if (!workspace_synced_at) {
const issueService = new IssueService();
return await issueService.subIssues(workspaceSlug, projectId, issueId);
@@ -412,74 +457,57 @@ export class Storage {
return await getSubIssuesWithDistribution(issueId);
};
getStatus = (projectId: string) => this.projectStatus[projectId]?.issues?.status || undefined;
setStatus = (projectId: string, status: "loading" | "ready" | "error" | "syncing" | undefined = undefined) => {
/**
* Gets the status of a project
* @param projectId - Project identifier
* @returns Project status or undefined
*/
public getStatus = (projectId: string): "loading" | "ready" | "error" | "syncing" | undefined =>
this.projectStatus[projectId]?.issues?.status || undefined;
/**
* Sets the status of a project
* @param projectId - Project identifier
* @param status - New status
*/
public setStatus = (
projectId: string,
status: "loading" | "ready" | "error" | "syncing" | undefined = undefined
): void => {
set(this.projectStatus, `${projectId}.issues.status`, status);
};
getSync = (projectId: string) => this.projectStatus[projectId]?.issues?.sync;
setSync = (projectId: string, sync: Promise<void> | undefined) => {
/**
* Gets the sync promise for a project
* @param projectId - Project identifier
* @returns Sync promise or undefined
*/
public getSync = (projectId: string): Promise<void> | undefined => this.projectStatus[projectId]?.issues?.sync;
/**
* Sets the sync promise for a project
* @param projectId - Project identifier
* @param sync - Sync promise
*/
public setSync = (projectId: string, sync: Promise<void> | undefined): void => {
set(this.projectStatus, `${projectId}.issues.sync`, sync);
};
getOption = async (key: string, fallback?: string | boolean | number) => {
try {
const options = await runQuery(`select * from options where key='${key}'`);
if (options.length) {
return options[0].value;
}
return fallback;
} catch (e) {
return fallback;
}
};
setOption = async (key: string, value: string) => {
await runQuery(`insert or replace into options (key, value) values ('${key}', '${value}')`);
};
deleteOption = async (key: string) => {
await runQuery(` DELETE FROM options where key='${key}'`);
};
getOptions = async (keys: string[]) => {
const options = await runQuery(`select * from options where key in ('${keys.join("','")}')`);
return options.reduce((acc: any, option: any) => {
acc[option.key] = option.value;
return acc;
}, {});
};
getIsWriteInProgress = async (op: string) => {
const writeStartTime = await this.getOption(op, false);
/**
* Checks if a write operation is in progress
* @param op - Operation identifier
* @returns Promise<boolean> - True if write is in progress
*/
public getIsWriteInProgress = async (op: string): Promise<boolean> => {
const writeStartTime = (await getOption(op, "")) as string;
if (writeStartTime) {
// Check if it has been more than 5seconds
const current = new Date();
const start = new Date(writeStartTime);
if (current.getTime() - start.getTime() < 5000) {
return true;
}
return false;
return current.getTime() - start.getTime() < 5000;
}
return false;
};
}
export const persistence = new Storage();
/**
* format the issue fetched from local db into an issue
* @param issue
* @returns
*/
export const formatLocalIssue = (issue: any) => {
const currIssue = issue;
ARRAY_FIELDS.forEach((field: string) => {
currIssue[field] = currIssue[field] ? JSON.parse(currIssue[field]) : [];
});
// Convert boolean fields to actual boolean values
BOOLEAN_FIELDS.forEach((field: string) => {
currIssue[field] = currIssue[field] === 1;
});
return currIssue as TIssue & { group_id?: string; total_issues: number; sub_group_id?: string };
};

View File

@@ -0,0 +1,116 @@
// Moving functions from storage.sqlite.ts to storage.sqlite.utils.ts
import { TIssue } from "@plane/types";
import { ARRAY_FIELDS, BOOLEAN_FIELDS } from "./constants";
import { runQuery } from "./query-executor";
/**
* Formats an issue fetched from local db into the required format
* @param issue - Raw issue data from database
* @returns Formatted issue with proper types
*/
export const formatLocalIssue = (
issue: any
): TIssue & { group_id?: string; total_issues: number; sub_group_id?: string } => {
const currIssue = { ...issue };
// Parse array fields from JSON strings
ARRAY_FIELDS.forEach((field: string) => {
currIssue[field] = currIssue[field] ? JSON.parse(currIssue[field]) : [];
});
// Convert boolean fields to actual boolean values
BOOLEAN_FIELDS.forEach((field: string) => {
currIssue[field] = currIssue[field] === 1;
});
return currIssue;
};
/**
* Gets the last updated issue for a project
* @param projectId - Project identifier
* @returns Promise<any | undefined> - Last updated issue or undefined
*/
export const getLastUpdatedIssue = async (projectId: string): Promise<any | undefined> => {
const lastUpdatedIssue = await runQuery(
`select id, name, updated_at, sequence_id from issues WHERE project_id='${projectId}' AND is_local_update IS NULL order by datetime(updated_at) desc limit 1`
);
return lastUpdatedIssue.length ? lastUpdatedIssue[0] : undefined;
};
/**
* Gets the last sync time for a project
* @param projectId - Project identifier
* @returns Promise<string | false> - Last sync time or false
*/
export const getLastSyncTime = async (projectId: string): Promise<string | false> => {
const issue = await getLastUpdatedIssue(projectId);
if (!issue) {
return false;
}
return issue.updated_at;
};
/**
* Gets the count of issues for a project
* @param projectId - Project identifier
* @returns Promise<number> - Count of issues
*/
export const getIssueCount = async (projectId: string): Promise<number> => {
const count = await runQuery(`select count(*) as count from issues where project_id='${projectId}'`);
return count[0]["count"];
};
/**
* Gets an option value
* @param key - Option key
* @param fallback - Fallback value
* @returns Option value or fallback
*/
export const getOption = async (
key: string,
fallback?: string | boolean | number
): Promise<string | boolean | number | undefined> => {
try {
const options = await runQuery(`select * from options where key='${key}'`);
if (options.length) {
return options[0].value;
}
return fallback;
} catch (e) {
return fallback;
}
};
/**
* Sets an option value
* @param key - Option key
* @param value - Option value
*/
export const setOption = async (key: string, value: string): Promise<void> => {
await runQuery(`insert or replace into options (key, value) values ('${key}', '${value}')`);
};
/**
* Deletes an option
* @param key - Option key
*/
export const deleteOption = async (key: string): Promise<void> => {
await runQuery(` DELETE FROM options where key='${key}'`);
};
/**
* Gets multiple options
* @param keys - Option keys
* @returns Options data
*/
export const getOptions = async (keys: string[]): Promise<Record<string, string | boolean | number>> => {
const options = await runQuery(`select * from options where key in ('${keys.join("','")}')`);
return options.reduce((acc: any, option: any) => {
acc[option.key] = option.value;
return acc;
}, {});
};

View File

@@ -19,7 +19,7 @@ export const logError = (e: any) => {
};
export const logInfo = console.info;
export const addIssueToPersistanceLayer = async (issue: TIssue) => {
export const addIssueToPersistenceLayer = async (issue: TIssue) => {
try {
const issuePartial = pick({ ...JSON.parse(JSON.stringify(issue)) }, [
"id",
@@ -66,7 +66,7 @@ export const updatePersistentLayer = async (issueIds: string | string[]) => {
const issue = rootStore.issue.issues.getIssueById(issueId);
if (issue) {
addIssueToPersistanceLayer(issue);
addIssueToPersistenceLayer(issue);
}
});
};

View File

@@ -72,6 +72,10 @@ export class DBClass {
async exec(props: string | TQueryProps) {
// @todo this will fail if the transaction is started any other way
// eg: BEGIN, OR BEGIN TRANSACTION
// Below code ensures the transactions are queued
// Ideally we should never have multiple transactions
// running at the same time, in rare cases, it shouldn't be waiting for more than 1ms
if (props === "BEGIN;") {
let promiseToAwait;
if (this.tp.length > 0) {

View File

@@ -5,7 +5,7 @@ import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } f
// helpers
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
// local db
import { addIssueToPersistanceLayer } from "@/local-db/utils/utils";
import { addIssueToPersistenceLayer } from "@/local-db/utils/utils";
// services
import { InboxIssueService } from "@/services/inbox";
import { IssueService } from "@/services/issue";
@@ -98,7 +98,7 @@ export class InboxIssueStore implements IInboxIssueStore {
// If issue accepted sync issue to local db
if (status === EInboxIssueStatus.ACCEPTED) {
addIssueToPersistanceLayer({ ...this.issue, ...inboxIssue.issue });
addIssueToPersistenceLayer({ ...this.issue, ...inboxIssue.issue });
}
} catch {
runInAction(() => set(this, "status", previousData.status));

View File

@@ -23,7 +23,7 @@ import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
// helpers
import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper";
// local-db
import { addIssueToPersistanceLayer } from "@/local-db/utils/utils";
import { addIssueToPersistenceLayer } from "@/local-db/utils/utils";
// services
import workspaceDraftService from "@/services/issue/workspace_draft.service";
// types
@@ -352,7 +352,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
}
// sync issue to local db
addIssueToPersistanceLayer({ ...payload, ...response });
addIssueToPersistenceLayer({ ...payload, ...response });
// Update draft issue count in workspaceUserInfo
this.updateWorkspaceUserDraftIssueCount(workspaceSlug, -1);