From 39d69be270e647ba41a743568f16df8ccabd1497 Mon Sep 17 00:00:00 2001 From: Guilherme Hermes <85593021+GuilhermeHermes@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:39:22 -0300 Subject: [PATCH 1/3] web worker to perform git operations --- src/global-state.ts | 25 +++---- src/hooks/attach-file.ts | 7 +- src/utils/fs.ts | 3 +- src/workers/worker-client.ts | 140 +++++++++++++++++++++++++++++++++++ src/workers/worker-types.ts | 60 +++++++++++++++ src/workers/worker.ts | 111 +++++++++++++++++++++++++++ 6 files changed, 327 insertions(+), 19 deletions(-) create mode 100644 src/workers/worker-client.ts create mode 100644 src/workers/worker-types.ts create mode 100644 src/workers/worker.ts diff --git a/src/global-state.ts b/src/global-state.ts index eaa00148..fe8078d5 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -19,14 +19,8 @@ import { fs, fsWipe } from "./utils/fs" import { REPO_DIR, getRemoteOriginUrl, - gitAdd, - gitClone, - gitCommit, - gitPull, - gitPush, - gitRemove, - isRepoSynced, } from "./utils/git" +import { worker } from "./workers/worker-client" import { parseNote } from "./utils/parse-note" import { removeTemplateFrontmatter } from "./utils/remove-template-frontmatter" import { getSampleMarkdownFiles } from "./utils/sample-markdown-files" @@ -341,7 +335,7 @@ function createGlobalStateMachine() { cloneRepo: async (context, event) => { if (!context.githubUser) throw new Error("Not signed in") - await gitClone(event.githubRepo, context.githubUser) + await worker.git.clone({ repo: event.githubRepo, user: context.githubUser }) return { markdownFiles: await getMarkdownFilesFromFs(REPO_DIR), @@ -350,7 +344,7 @@ function createGlobalStateMachine() { pull: async (context) => { if (!context.githubUser) throw new Error("Not signed in") - await gitPull(context.githubUser) + await worker.git.pull({ user: context.githubUser }) return { markdownFiles: await getMarkdownFilesFromFs(REPO_DIR), @@ -359,10 +353,11 @@ function createGlobalStateMachine() { push: async (context) => { if (!context.githubUser) throw new Error("Not signed in") - await gitPush(context.githubUser) + await worker.git.push({ user: context.githubUser }) }, checkStatus: async () => { - return { isSynced: await isRepoSynced() } + const result = await worker.git.status({}) + return { isSynced: result.isSynced } }, writeFiles: async (context, event) => { if (!context.githubUser) throw new Error("Not signed in") @@ -395,10 +390,10 @@ function createGlobalStateMachine() { }) // Stage files - await gitAdd(Object.keys(markdownFiles)) + await worker.git.add({ filePaths: Object.keys(markdownFiles) }) // Commit files - await gitCommit(commitMessage) + await worker.git.commit({ message: commitMessage }) }, deleteFile: async (context, event) => { if (!context.githubUser) throw new Error("Not signed in") @@ -409,10 +404,10 @@ function createGlobalStateMachine() { await fs.promises.unlink(`${REPO_DIR}/${filepath}`) // Stage deletion - await gitRemove(filepath) + await worker.git.remove({ filePath: filepath }) // Commit deletion - await gitCommit(`Delete ${filepath}`) + await worker.git.commit({ message: `Delete ${filepath}` }) }, }, actions: { diff --git a/src/hooks/attach-file.ts b/src/hooks/attach-file.ts index c93b398b..6116a9c4 100644 --- a/src/hooks/attach-file.ts +++ b/src/hooks/attach-file.ts @@ -4,7 +4,8 @@ import React from "react" import { fileCache } from "../components/file-preview" import { githubRepoAtom, githubUserAtom } from "../global-state" import { fs, writeFile } from "../utils/fs" -import { REPO_DIR, gitAdd, gitCommit } from "../utils/git" +import { REPO_DIR } from "../utils/git" +import { worker } from "../workers/worker-client" export const UPLOADS_DIR = "/uploads" @@ -46,10 +47,10 @@ export function useAttachFile() { const relativePath = path.replace(/^\//, "") // Stage file - await gitAdd([relativePath]) + await worker.git.add({ filePaths: [relativePath] }) // Commit file - await gitCommit(`Update ${relativePath}`) + await worker.git.commit({ message: `Update ${relativePath}` }) }) .catch((error) => { console.error(error) diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 8fca6856..d68237cf 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -17,7 +17,8 @@ export const fs = new LightningFS(DB_NAME) /** Delete file system database */ export function fsWipe() { - window.indexedDB.deleteDatabase(DB_NAME) + const indexedDB = globalThis.indexedDB + indexedDB.deleteDatabase(DB_NAME) } /** diff --git a/src/workers/worker-client.ts b/src/workers/worker-client.ts new file mode 100644 index 00000000..fd6d9671 --- /dev/null +++ b/src/workers/worker-client.ts @@ -0,0 +1,140 @@ +import type { + WorkerRequest, + WorkerResponse, + GitCloneRequest, + GitPullRequest, + GitPushRequest, + GitCommitRequest, + GitAddRequest, + GitRemoveRequest, + GitStatusRequest +} from './worker-types' + +class WorkerClient { + private worker: Worker | null = null + private requestId = 0 + private pendingRequests = new Map void + reject: (error: Error) => void + }>() + + constructor() { + this.initWorker() + } + + private initWorker() { + try { + this.worker = new Worker( + new URL('./worker.ts', import.meta.url), + { type: 'module' } + ) + + this.worker.onmessage = (event: MessageEvent) => { + const { data } = event + const pending = this.pendingRequests.get(data.id) + + if (!pending) { + console.warn('Received response for unknown request ID:', data.id) + return + } + + this.pendingRequests.delete(data.id) + + if (data.type === 'error') { + const errorPayload = data.payload as { error?: string } + const errorMessage = errorPayload?.error || 'Unknown error' + pending.reject(new Error(errorMessage)) + } else if (data.type === 'success') { + pending.resolve(data.payload) + } + } + + this.worker.onerror = (error) => { + console.error('Worker error:', error) + for (const [id, pending] of this.pendingRequests) { + pending.reject(new Error('Worker error')) + this.pendingRequests.delete(id) + } + } + } catch (error) { + console.error('Failed to initialize worker:', error) + } + } + + private sendRequest(category: 'git', type: string, payload: WorkerRequest['payload']): Promise { + return new Promise((resolve, reject) => { + if (!this.worker) { + reject(new Error('Worker not available')) + return + } + + const id = (++this.requestId).toString() + const request: WorkerRequest = { id, category, type, payload } + + this.pendingRequests.set(id, { + resolve: resolve as (value: unknown) => void, + reject + }) + this.worker.postMessage(request) + + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id) + reject(new Error('Request timeout')) + } + }, 30000) + }) + } + + // Git operations + git = { + clone: (options: GitCloneRequest): Promise => { + return this.sendRequest('git', 'clone', options) + }, + + pull: (options: GitPullRequest): Promise => { + return this.sendRequest('git', 'pull', options) + }, + + push: (options: GitPushRequest): Promise => { + return this.sendRequest('git', 'push', options) + }, + + commit: (options: GitCommitRequest): Promise => { + return this.sendRequest('git', 'commit', options) + }, + + add: (options: GitAddRequest): Promise => { + return this.sendRequest('git', 'add', options) + }, + + remove: (options: GitRemoveRequest): Promise => { + return this.sendRequest('git', 'remove', options) + }, + + status: (options: GitStatusRequest = {}): Promise<{ isSynced: boolean }> => { + return this.sendRequest<{ isSynced: boolean }>('git', 'status', options) + } + } + + dispose() { + if (this.worker) { + for (const [id, pending] of this.pendingRequests) { + pending.reject(new Error('Worker disposed')) + this.pendingRequests.delete(id) + } + + this.worker.terminate() + this.worker = null + } + } +} + +// Singleton +export const worker = new WorkerClient() + +if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', () => { + worker.dispose() + }) +} diff --git a/src/workers/worker-types.ts b/src/workers/worker-types.ts new file mode 100644 index 00000000..a705b400 --- /dev/null +++ b/src/workers/worker-types.ts @@ -0,0 +1,60 @@ +// Types for communication between main thread and worker + +export interface WorkerRequest { + id: string + category: 'git' + type: string + payload: GitCloneRequest | GitPullRequest | GitPushRequest | GitCommitRequest | GitAddRequest | GitRemoveRequest | GitStatusRequest +} + +export interface WorkerResponse { + id: string + type: 'success' | 'error' + payload: unknown +} + +export interface GitCloneRequest { + repo: { + owner: string + name: string + } + user: { + login: string + name: string + email: string + token: string + } +} + +export interface GitPullRequest { + user: { + login: string + name: string + email: string + token: string + } +} + +export interface GitPushRequest { + user: { + login: string + name: string + email: string + token: string + } +} + +export interface GitCommitRequest { + message: string +} + +export interface GitAddRequest { + filePaths: string[] +} + +export interface GitRemoveRequest { + filePath: string +} + +export interface GitStatusRequest { +} diff --git a/src/workers/worker.ts b/src/workers/worker.ts new file mode 100644 index 00000000..edc6d6f5 --- /dev/null +++ b/src/workers/worker.ts @@ -0,0 +1,111 @@ +// Worker - Runs all heavy operations in a separate thread +// This prevents blocking the main UI thread + +import { + gitClone, + gitPull, + gitPush, + gitCommit, + gitAdd, + gitRemove, + isRepoSynced, +} from "../utils/git" +import type { + WorkerRequest, + WorkerResponse, + GitCloneRequest, + GitPullRequest, + GitPushRequest, + GitCommitRequest, + GitAddRequest, + GitRemoveRequest +} from "./worker-types" + +// Helper function to send success response +function sendSuccess(id: string, result?: unknown) { + const response: WorkerResponse = { + id, + type: 'success', + payload: result || {} + } + + self.postMessage(response) +} + +// Helper function to send error response +function sendError(id: string, error: Error | string) { + const response: WorkerResponse = { + id, + type: 'error', + payload: { error: error instanceof Error ? error.message : error } + } + + self.postMessage(response) +} + +// Message handler +self.onmessage = async (event: MessageEvent) => { + const request = event.data + + try { + if (request.category === 'git') { + switch (request.type) { + case 'clone': { + const payload = request.payload as GitCloneRequest + await gitClone(payload.repo, payload.user) + sendSuccess(request.id) + break + } + + case 'pull': { + const payload = request.payload as GitPullRequest + await gitPull(payload.user) + sendSuccess(request.id) + break + } + + case 'push': { + const payload = request.payload as GitPushRequest + await gitPush(payload.user) + sendSuccess(request.id) + break + } + + case 'commit': { + const payload = request.payload as GitCommitRequest + await gitCommit(payload.message) + sendSuccess(request.id) + break + } + + case 'add': { + const payload = request.payload as GitAddRequest + await gitAdd(payload.filePaths) + sendSuccess(request.id) + break + } + + case 'remove': { + const payload = request.payload as GitRemoveRequest + await gitRemove(payload.filePath) + sendSuccess(request.id) + break + } + + case 'status': { + const isSynced = await isRepoSynced() + sendSuccess(request.id, { isSynced }) + break + } + + default: + sendError(request.id, `Unknown git operation: ${request.type}`) + } + } else { + sendError(request.id, `Unknown category: ${request.category}`) + } + } catch (error) { + console.error('Worker error:', error) + sendError(request.id, error instanceof Error ? error : String(error)) + } +} From 1a70620c9b953b4f95d3d45ab57504818d7e2af1 Mon Sep 17 00:00:00 2001 From: Guilherme Hermes <85593021+GuilhermeHermes@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:47:02 -0300 Subject: [PATCH 2/3] fix names --- src/global-state.ts | 2 +- src/hooks/attach-file.ts | 2 +- src/{workers/worker-client.ts => utils/git-worker-client.ts} | 2 +- src/{workers/worker-types.ts => utils/git-worker-types.ts} | 0 src/{workers/worker.ts => utils/git-worker.ts} | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{workers/worker-client.ts => utils/git-worker-client.ts} (99%) rename src/{workers/worker-types.ts => utils/git-worker-types.ts} (100%) rename src/{workers/worker.ts => utils/git-worker.ts} (98%) diff --git a/src/global-state.ts b/src/global-state.ts index fe8078d5..2bb5eb61 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -20,7 +20,7 @@ import { REPO_DIR, getRemoteOriginUrl, } from "./utils/git" -import { worker } from "./workers/worker-client" +import { worker } from "./utils/git-worker-client" import { parseNote } from "./utils/parse-note" import { removeTemplateFrontmatter } from "./utils/remove-template-frontmatter" import { getSampleMarkdownFiles } from "./utils/sample-markdown-files" diff --git a/src/hooks/attach-file.ts b/src/hooks/attach-file.ts index 6116a9c4..7ac932ed 100644 --- a/src/hooks/attach-file.ts +++ b/src/hooks/attach-file.ts @@ -5,7 +5,7 @@ import { fileCache } from "../components/file-preview" import { githubRepoAtom, githubUserAtom } from "../global-state" import { fs, writeFile } from "../utils/fs" import { REPO_DIR } from "../utils/git" -import { worker } from "../workers/worker-client" +import { worker } from "../utils/git-worker-client" export const UPLOADS_DIR = "/uploads" diff --git a/src/workers/worker-client.ts b/src/utils/git-worker-client.ts similarity index 99% rename from src/workers/worker-client.ts rename to src/utils/git-worker-client.ts index fd6d9671..f855e8a9 100644 --- a/src/workers/worker-client.ts +++ b/src/utils/git-worker-client.ts @@ -8,7 +8,7 @@ import type { GitAddRequest, GitRemoveRequest, GitStatusRequest -} from './worker-types' +} from './git-worker-types' class WorkerClient { private worker: Worker | null = null diff --git a/src/workers/worker-types.ts b/src/utils/git-worker-types.ts similarity index 100% rename from src/workers/worker-types.ts rename to src/utils/git-worker-types.ts diff --git a/src/workers/worker.ts b/src/utils/git-worker.ts similarity index 98% rename from src/workers/worker.ts rename to src/utils/git-worker.ts index edc6d6f5..cad5648f 100644 --- a/src/workers/worker.ts +++ b/src/utils/git-worker.ts @@ -9,7 +9,7 @@ import { gitAdd, gitRemove, isRepoSynced, -} from "../utils/git" +} from "./git" import type { WorkerRequest, WorkerResponse, @@ -19,7 +19,7 @@ import type { GitCommitRequest, GitAddRequest, GitRemoveRequest -} from "./worker-types" +} from "./git-worker-types" // Helper function to send success response function sendSuccess(id: string, result?: unknown) { From 24a359c489f458456f7a489f405ef3ee08967a47 Mon Sep 17 00:00:00 2001 From: Guilherme Hermes <85593021+GuilhermeHermes@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:06:44 -0300 Subject: [PATCH 3/3] hotfix worker instance --- src/utils/git-worker-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/git-worker-client.ts b/src/utils/git-worker-client.ts index f855e8a9..7655befd 100644 --- a/src/utils/git-worker-client.ts +++ b/src/utils/git-worker-client.ts @@ -25,7 +25,7 @@ class WorkerClient { private initWorker() { try { this.worker = new Worker( - new URL('./worker.ts', import.meta.url), + new URL('./git-worker.ts', import.meta.url), { type: 'module' } )