Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions src/global-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "./utils/git-worker-client"
import { parseNote } from "./utils/parse-note"
import { removeTemplateFrontmatter } from "./utils/remove-template-frontmatter"
import { getSampleMarkdownFiles } from "./utils/sample-markdown-files"
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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: {
Expand Down
7 changes: 4 additions & 3 deletions src/hooks/attach-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "../utils/git-worker-client"

export const UPLOADS_DIR = "/uploads"

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/utils/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
140 changes: 140 additions & 0 deletions src/utils/git-worker-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type {
WorkerRequest,
WorkerResponse,
GitCloneRequest,
GitPullRequest,
GitPushRequest,
GitCommitRequest,
GitAddRequest,
GitRemoveRequest,
GitStatusRequest
} from './git-worker-types'

class WorkerClient {
private worker: Worker | null = null
private requestId = 0
private pendingRequests = new Map<string, {
resolve: (value: unknown) => void
reject: (error: Error) => void
}>()

constructor() {
this.initWorker()
}

private initWorker() {
try {
this.worker = new Worker(
new URL('./git-worker.ts', import.meta.url),
{ type: 'module' }
)

this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
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<T>(category: 'git', type: string, payload: WorkerRequest['payload']): Promise<T> {
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<void> => {
return this.sendRequest<void>('git', 'clone', options)
},

pull: (options: GitPullRequest): Promise<void> => {
return this.sendRequest<void>('git', 'pull', options)
},

push: (options: GitPushRequest): Promise<void> => {
return this.sendRequest<void>('git', 'push', options)
},

commit: (options: GitCommitRequest): Promise<void> => {
return this.sendRequest<void>('git', 'commit', options)
},

add: (options: GitAddRequest): Promise<void> => {
return this.sendRequest<void>('git', 'add', options)
},

remove: (options: GitRemoveRequest): Promise<void> => {
return this.sendRequest<void>('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()
})
}
60 changes: 60 additions & 0 deletions src/utils/git-worker-types.ts
Original file line number Diff line number Diff line change
@@ -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 {
}
Loading