2024-10-24 15:35:02 +05:30
import * as Comlink from "comlink" ;
2024-09-24 19:01:34 +05:30
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" ;
//
2024-11-08 17:09:26 +05:30
import { ARRAY_FIELDS , BOOLEAN_FIELDS } from "./utils/constants" ;
2024-09-24 19:01:34 +05:30
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 { createTables } from "./utils/tables" ;
2024-10-24 15:35:02 +05:30
import { clearOPFS , getGroupedIssueResults , getSubGroupedIssueResults , log , logError } from "./utils/utils" ;
2024-09-24 19:01:34 +05:30
const DB_VERSION = 1 ;
2024-10-24 15:35:02 +05:30
const PAGE_SIZE = 500 ;
2024-11-26 19:12:39 +05:30
const BATCH_SIZE = 50 ;
2024-09-24 19:01:34 +05:30
type TProjectStatus = {
issues : { status : undefined | "loading" | "ready" | "error" | "syncing" ; sync : Promise < void > | undefined } ;
} ;
type TDBStatus = "initializing" | "ready" | "error" | undefined ;
export class Storage {
db : any ;
status : TDBStatus = undefined ;
dbName = "plane" ;
projectStatus : Record < string , TProjectStatus > = { } ;
workspaceSlug : string = "" ;
constructor ( ) {
this . db = null ;
2024-11-04 16:54:13 +05:30
if ( typeof window !== "undefined" ) {
window . addEventListener ( "beforeunload" , this . closeDBConnection ) ;
}
2024-09-24 19:01:34 +05:30
}
2024-11-04 16:54:13 +05:30
closeDBConnection = async ( ) = > {
if ( this . db ) {
await this . db . close ( ) ;
}
} ;
2024-09-24 19:01:34 +05:30
reset = ( ) = > {
2024-10-24 15:35:02 +05:30
if ( this . db ) {
this . db . close ( ) ;
}
2024-09-24 19:01:34 +05:30
this . db = null ;
this . status = undefined ;
this . projectStatus = { } ;
this . workspaceSlug = "" ;
} ;
2024-12-17 17:46:24 +05:30
clearStorage = async ( force = false ) = > {
2024-09-24 19:01:34 +05:30
try {
2024-12-17 17:46:24 +05:30
await this . db ? . close ( ) ;
await clearOPFS ( force ) ;
2024-09-27 15:11:38 +05:30
this . reset ( ) ;
2024-09-24 19:01:34 +05:30
} catch ( e ) {
console . error ( "Error clearing sqlite sync storage" , e ) ;
}
} ;
initialize = async ( workspaceSlug : string ) : Promise < boolean > = > {
2024-10-24 15:35:02 +05:30
if ( ! rootStore . user . localDBEnabled ) return false ; // return if the window gets hidden
2024-09-24 19:01:34 +05:30
if ( workspaceSlug !== this . workspaceSlug ) {
this . reset ( ) ;
}
2024-11-08 17:09:26 +05:30
2024-09-24 19:01:34 +05:30
try {
2025-01-03 15:01:36 +05:30
await this . _initialize ( workspaceSlug ) ;
2024-09-24 19:01:34 +05:30
return true ;
} catch ( err ) {
2024-09-26 14:04:59 +05:30
logError ( err ) ;
2024-09-24 19:01:34 +05:30
this . status = "error" ;
return false ;
}
} ;
_initialize = async ( workspaceSlug : string ) : Promise < boolean > = > {
if ( this . status === "initializing" ) {
console . warn ( ` Initialization already in progress for workspace ${ workspaceSlug } ` ) ;
return false ;
}
if ( this . status === "ready" ) {
console . warn ( ` Already initialized for workspace ${ workspaceSlug } ` ) ;
return true ;
}
if ( this . status === "error" ) {
console . warn ( ` Initialization failed for workspace ${ workspaceSlug } ` ) ;
return false ;
}
try {
2024-10-24 15:35:02 +05:30
const { DBClass } = await import ( "./worker/db" ) ;
const worker = new Worker ( new URL ( "./worker/db.ts" , import . meta . url ) ) ;
const MyWorker = Comlink . wrap < typeof DBClass > ( worker ) ;
2024-09-24 19:01:34 +05:30
2024-10-24 15:35:02 +05:30
// Add cleanup on window unload
window . addEventListener ( "unload" , ( ) = > worker . terminate ( ) ) ;
this . workspaceSlug = workspaceSlug ;
this . dbName = workspaceSlug ;
const instance = await new MyWorker ( ) ;
await instance . init ( workspaceSlug ) ;
2024-09-24 19:01:34 +05:30
this . db = {
2024-10-24 15:35:02 +05:30
exec : instance.exec ,
close : instance.close ,
2024-09-24 19:01:34 +05:30
} ;
this . status = "ready" ;
// Your SQLite code here.
await createTables ( ) ;
await this . setOption ( "DB_VERSION" , DB_VERSION . toString ( ) ) ;
2024-10-24 15:35:02 +05:30
return true ;
} catch ( error ) {
this . status = "error" ;
2024-11-08 17:09:26 +05:30
this . db = null ;
2024-10-24 15:35:02 +05:30
throw new Error ( ` Failed to initialize database worker: ${ error } ` ) ;
2024-09-24 19:01:34 +05:30
}
} ;
syncWorkspace = async ( ) = > {
2024-10-24 15:35:02 +05:30
if ( ! rootStore . user . localDBEnabled ) return ;
const syncInProgress = await this . getIsWriteInProgress ( "sync_workspace" ) ;
if ( syncInProgress ) {
log ( "Sync in progress, skipping" ) ;
return ;
}
try {
2025-01-03 15:01:36 +05:30
this . setOption ( "sync_workspace" , new Date ( ) . toUTCString ( ) ) ;
await loadWorkSpaceData ( this . workspaceSlug ) ;
this . deleteOption ( "sync_workspace" ) ;
2024-10-24 15:35:02 +05:30
} catch ( e ) {
logError ( e ) ;
this . deleteOption ( "sync_workspace" ) ;
}
2024-09-24 19:01:34 +05:30
} ;
syncProject = async ( projectId : string ) = > {
2024-10-24 15:35:02 +05:30
if (
// document.hidden ||
! rootStore . user . localDBEnabled
)
return false ; // return if the window gets hidden
2024-09-24 19:01:34 +05:30
// Load labels, members, states, modules, cycles
await this . syncIssues ( projectId ) ;
// // Sync rest of the projects
// const projects = await getProjectIds();
// // Exclude the one we just synced
// const projectsToSync = projects.filter((p: string) => p !== projectId);
// for (const project of projectsToSync) {
// await delay(8000);
// await this.syncIssues(project);
// }
// this.setOption("workspace_synced_at", new Date().toISOString());
} ;
syncIssues = async ( projectId : string ) = > {
2024-10-24 15:35:02 +05:30
if ( ! rootStore . user . localDBEnabled || ! this . db ) {
return false ;
}
2024-09-24 19:01:34 +05:30
try {
2025-01-03 15:01:36 +05:30
const sync = this . _syncIssues ( projectId ) ;
2024-09-24 19:01:34 +05:30
this . setSync ( projectId , sync ) ;
await sync ;
} catch ( e ) {
2024-09-26 14:04:59 +05:30
logError ( e ) ;
2024-09-24 19:01:34 +05:30
this . setStatus ( projectId , "error" ) ;
}
} ;
_syncIssues = async ( projectId : string ) = > {
2024-09-26 14:04:59 +05:30
log ( "### Sync started" ) ;
2024-09-24 19:01:34 +05:30
let status = this . getStatus ( projectId ) ;
if ( status === "loading" || status === "syncing" ) {
2024-10-24 15:35:02 +05:30
log ( ` Project ${ projectId } is already loading or syncing ` ) ;
2024-09-24 19:01:34 +05:30
return ;
}
const syncPromise = this . getSync ( projectId ) ;
if ( syncPromise ) {
// Redundant check?
return ;
}
const queryParams : { cursor : string ; updated_at__gt? : string ; description : boolean } = {
cursor : ` ${ PAGE_SIZE } :0:0 ` ,
description : true ,
} ;
const syncedAt = await this . getLastSyncTime ( projectId ) ;
const projectSync = await this . getOption ( projectId ) ;
if ( syncedAt ) {
queryParams [ "updated_at__gt" ] = syncedAt ;
}
this . setStatus ( projectId , projectSync === "ready" ? "syncing" : "loading" ) ;
status = this . getStatus ( projectId ) ;
log ( ` ### ${ projectSync === "ready" ? "Syncing" : "Loading" } issues to local db for project ${ projectId } ` ) ;
const start = performance . now ( ) ;
const issueService = new IssueService ( ) ;
const response = await issueService . getIssuesForSync ( this . workspaceSlug , projectId , queryParams ) ;
2024-10-24 15:35:02 +05:30
await addIssuesBulk ( response . results , BATCH_SIZE ) ;
2024-09-24 19:01:34 +05:30
if ( response . total_pages > 1 ) {
const promiseArray = [ ] ;
for ( let i = 1 ; i < response . total_pages ; i ++ ) {
queryParams . cursor = ` ${ PAGE_SIZE } : ${ i } :0 ` ;
promiseArray . push ( issueService . getIssuesForSync ( this . workspaceSlug , projectId , queryParams ) ) ;
}
const pages = await Promise . all ( promiseArray ) ;
for ( const page of pages ) {
await addIssuesBulk ( page . results , BATCH_SIZE ) ;
}
}
if ( syncedAt ) {
await syncDeletesToLocal ( this . workspaceSlug , projectId , { updated_at__gt : syncedAt } ) ;
}
2025-02-06 20:41:31 +05:30
log ( "### Time taken to add work items" , performance . now ( ) - start ) ;
2024-09-24 19:01:34 +05:30
if ( status === "loading" ) {
await createIndexes ( ) ;
}
this . setOption ( projectId , "ready" ) ;
this . setStatus ( projectId , "ready" ) ;
this . setSync ( projectId , undefined ) ;
} ;
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 (
2024-09-26 14:04:59 +05:30
` 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 `
2024-09-24 19:01:34 +05:30
) ;
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 ) = > {
2024-09-26 14:04:59 +05:30
log ( "#### Queries" , queries ) ;
2024-09-24 19:01:34 +05:30
const currentProjectStatus = this . getStatus ( projectId ) ;
if (
! currentProjectStatus ||
this . status !== "ready" ||
currentProjectStatus === "loading" ||
currentProjectStatus === "error" ||
! rootStore . user . localDBEnabled
) {
2024-10-01 14:18:01 +05:30
if ( rootStore . user . localDBEnabled ) {
2024-10-24 15:35:02 +05:30
log ( ` Project ${ projectId } is loading, falling back to server ` ) ;
2024-10-01 14:18:01 +05:30
}
2024-09-24 19:01:34 +05:30
const issueService = new IssueService ( ) ;
2024-12-06 16:27:07 +05:30
// Ignore projectStatus if projectId is not provided
if ( projectId ) {
return await issueService . getIssuesFromServer ( workspaceSlug , projectId , queries , config ) ;
}
if ( this . status !== "ready" && ! rootStore . user . localDBEnabled ) {
return ;
}
2024-09-24 19:01:34 +05:30
}
const { cursor , group_by , sub_group_by } = queries ;
const query = issueFilterQueryConstructor ( this . workspaceSlug , projectId , queries ) ;
2024-11-13 15:38:43 +05:30
log ( "#### Query" , query ) ;
2024-09-24 19:01:34 +05:30
const countQuery = issueFilterCountQueryConstructor ( this . workspaceSlug , projectId , queries ) ;
const start = performance . now ( ) ;
2024-10-01 14:18:01 +05:30
let issuesRaw : any [ ] = [ ] ;
let count : any [ ] ;
try {
2025-01-03 15:01:36 +05:30
[ issuesRaw , count ] = await Promise . all ( [ runQuery ( query ) , runQuery ( countQuery ) ] ) ;
2024-10-01 14:18:01 +05:30
} catch ( e ) {
logError ( e ) ;
const issueService = new IssueService ( ) ;
2024-11-05 17:50:23 +05:30
return await issueService . getIssuesFromServer ( workspaceSlug , projectId , queries , config ) ;
2024-10-01 14:18:01 +05:30
}
2024-09-24 19:01:34 +05:30
const end = performance . now ( ) ;
const { total_count } = count [ 0 ] ;
const [ pageSize , page , offset ] = cursor . split ( ":" ) ;
const groupByProperty : string =
EIssueGroupBYServerToProperty [ group_by as keyof typeof EIssueGroupBYServerToProperty ] ;
const subGroupByProperty =
EIssueGroupBYServerToProperty [ sub_group_by as keyof typeof EIssueGroupBYServerToProperty ] ;
const parsingStart = performance . now ( ) ;
let issueResults = issuesRaw . map ( ( issue : any ) = > formatLocalIssue ( issue ) ) ;
2025-02-06 20:41:31 +05:30
log ( "#### Work item Results" , issueResults . length ) ;
2024-09-24 19:01:34 +05:30
const parsingEnd = performance . now ( ) ;
const grouping = performance . now ( ) ;
if ( groupByProperty && page === "0" ) {
if ( subGroupByProperty ) {
issueResults = getSubGroupedIssueResults ( issueResults ) ;
} else {
issueResults = getGroupedIssueResults ( issueResults ) ;
}
}
2024-10-01 14:18:01 +05:30
const groupCount = group_by ? Object . keys ( issueResults ) . length : undefined ;
2024-10-01 15:31:04 +05:30
// const subGroupCount = sub_group_by ? Object.keys(issueResults[Object.keys(issueResults)[0]]).length : undefined;
2024-09-24 19:01:34 +05:30
const groupingEnd = performance . now ( ) ;
const times = {
IssueQuery : end - start ,
Parsing : parsingEnd - parsingStart ,
Grouping : groupingEnd - grouping ,
} ;
2024-10-01 15:31:04 +05:30
if ( ( window as any ) . DEBUG ) {
console . table ( times ) ;
}
2024-09-24 19:01:34 +05:30
const total_pages = Math . ceil ( total_count / Number ( pageSize ) ) ;
const next_page_results = total_pages > parseInt ( page ) + 1 ;
const out = {
results : issueResults ,
next_cursor : ` ${ pageSize } : ${ parseInt ( page ) + 1 } : ${ Number ( offset ) + Number ( pageSize ) } ` ,
prev_cursor : ` ${ pageSize } : ${ parseInt ( page ) - 1 } : ${ Number ( offset ) - Number ( pageSize ) } ` ,
total_results : total_count ,
total_count ,
next_page_results ,
total_pages ,
} ;
return out ;
} ;
getIssue = async ( issueId : string ) = > {
try {
2024-11-26 19:12:39 +05:30
if ( ! rootStore . user . localDBEnabled || this . status !== "ready" ) return ;
2024-09-24 19:01:34 +05:30
const issues = await runQuery ( ` select * from issues where id=' ${ issueId } ' ` ) ;
2024-11-26 19:12:39 +05:30
if ( Array . isArray ( issues ) && issues . length ) {
2024-09-24 19:01:34 +05:30
return formatLocalIssue ( issues [ 0 ] ) ;
}
} catch ( err ) {
2024-10-01 14:18:01 +05:30
logError ( err ) ;
2024-09-24 19:01:34 +05:30
console . warn ( "unable to fetch issue from local db" ) ;
}
return ;
} ;
getSubIssues = async ( workspaceSlug : string , projectId : string , issueId : string ) = > {
const workspace_synced_at = await this . getOption ( "workspace_synced_at" ) ;
if ( ! workspace_synced_at ) {
const issueService = new IssueService ( ) ;
return await issueService . subIssues ( workspaceSlug , projectId , issueId ) ;
}
return await getSubIssuesWithDistribution ( issueId ) ;
} ;
getStatus = ( projectId : string ) = > this . projectStatus [ projectId ] ? . issues ? . status || undefined ;
setStatus = ( projectId : string , status : "loading" | "ready" | "error" | "syncing" | undefined = undefined ) = > {
set ( this . projectStatus , ` ${ projectId } .issues.status ` , status ) ;
} ;
getSync = ( projectId : string ) = > this . projectStatus [ projectId ] ? . issues ? . sync ;
setSync = ( projectId : string , sync : Promise < void > | undefined ) = > {
set ( this . projectStatus , ` ${ projectId } .issues.sync ` , sync ) ;
} ;
2024-10-24 15:35:02 +05:30
getOption = async ( key : string , fallback? : string | boolean | number ) = > {
2024-09-24 19:01:34 +05:30
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 } ') ` ) ;
} ;
2024-10-24 15:35:02 +05:30
deleteOption = async ( key : string ) = > {
await runQuery ( ` DELETE FROM options where key=' ${ key } ' ` ) ;
} ;
2024-09-24 19:01:34 +05:30
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 ;
} , { } ) ;
} ;
2024-10-24 15:35:02 +05:30
getIsWriteInProgress = async ( op : string ) = > {
const writeStartTime = await this . getOption ( op , false ) ;
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 false ;
} ;
2024-09-24 19:01:34 +05:30
}
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 ] ) : [ ] ;
} ) ;
2024-11-08 17:09:26 +05:30
// Convert boolean fields to actual boolean values
BOOLEAN_FIELDS . forEach ( ( field : string ) = > {
currIssue [ field ] = currIssue [ field ] === 1 ;
} ) ;
2024-10-01 15:31:04 +05:30
return currIssue as TIssue & { group_id? : string ; total_issues : number ; sub_group_id? : string } ;
2024-09-24 19:01:34 +05:30
} ;