Skip to content

Commit a807196

Browse files
adjusted ws server for new supabase storage and session_files tables
1 parent e5b2332 commit a807196

File tree

2 files changed

+442
-95
lines changed

2 files changed

+442
-95
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { config } from "dotenv";
2+
config();
3+
import { createClient } from "@supabase/supabase-js";
4+
import * as Y from "yjs";
5+
6+
const supabase = createClient(
7+
process.env.SUPABASE_URL,
8+
process.env.SUPABASE_ANON_KEY
9+
);
10+
11+
export class YjsPersistence {
12+
constructor() {
13+
this.docs = new Map();
14+
this.saveTimeouts = new Map();
15+
this.SAVE_DELAY = 3000; // 3 seconds
16+
}
17+
18+
async bindState(workspaceId, ydoc) {
19+
console.log(`Binding state for workspace: ${workspaceId}`);
20+
this.docs.set(workspaceId, ydoc);
21+
22+
await this.loadFromDatabase(workspaceId, ydoc);
23+
this.setupUpdateHandler(workspaceId, ydoc);
24+
}
25+
26+
async loadFromDatabase(workspaceId, ydoc) {
27+
try {
28+
const { data: files, error } = await supabase
29+
.from("files")
30+
.select("id, name, file_type, storage_path, content")
31+
.eq("workspace_id", workspaceId);
32+
33+
if (error) {
34+
console.error(`Error loading files for ${workspaceId}:`, error);
35+
return;
36+
}
37+
38+
console.log(
39+
`Loading ${files?.length || 0} files for workspace: ${workspaceId}`
40+
);
41+
42+
if (files && files.length > 0) {
43+
const fileSystemMap = ydoc.getMap("fileSystem");
44+
45+
// Convert flat file list to nested structure
46+
const createNestedStructure = (filesList) => {
47+
const root = [];
48+
const pathMap = new Map(); // To track created folders
49+
50+
filesList.forEach((file) => {
51+
const filePath = file.storage_path || file.name;
52+
const pathParts = filePath.split("/").filter(Boolean);
53+
const fileName = pathParts.pop() || file.name;
54+
55+
let currentLevel = root;
56+
let currentPath = "";
57+
58+
// Create folders as needed
59+
for (const part of pathParts) {
60+
currentPath += (currentPath ? "/" : "") + part;
61+
62+
// Check if folder already exists at this level
63+
let folder = currentLevel.find(
64+
(item) => item.type === "folder" && item.name === part
65+
);
66+
67+
if (!folder) {
68+
// Create folder if it doesn't exist
69+
const folderId = `folder-${currentPath.replace(
70+
/[\/\\]/g,
71+
"-"
72+
)}`;
73+
folder = {
74+
id: folderId,
75+
name: part,
76+
type: "folder",
77+
children: [],
78+
};
79+
currentLevel.push(folder);
80+
pathMap.set(currentPath, folder);
81+
}
82+
83+
currentLevel = folder.children;
84+
}
85+
86+
// Add file to the current level
87+
currentLevel.push({
88+
id: file.id,
89+
name: fileName,
90+
type: file.file_type || "file",
91+
path: file.storage_path,
92+
});
93+
});
94+
95+
return root;
96+
};
97+
98+
const nestedFiles = createNestedStructure(files);
99+
fileSystemMap.set("files", nestedFiles);
100+
101+
files.forEach((file) => {
102+
if (file.content) {
103+
const fileText = ydoc.getText(`file-${file.id}`);
104+
if (fileText.length === 0) {
105+
fileText.insert(0, file.content);
106+
}
107+
}
108+
});
109+
}
110+
} catch (error) {
111+
console.error(
112+
`Failed to load from database for workspace ${workspaceId}:`,
113+
error
114+
);
115+
}
116+
}
117+
118+
setupUpdateHandler(workspaceId, ydoc) {
119+
ydoc.on("update", (update, origin) => {
120+
if (origin !== "persistence") {
121+
this.debouncedSave(workspaceId, ydoc);
122+
}
123+
});
124+
}
125+
126+
debouncedSave(workspaceId, ydoc) {
127+
// Clear existing timeout
128+
if (this.saveTimeouts.has(workspaceId)) {
129+
clearTimeout(this.saveTimeouts.get(workspaceId));
130+
}
131+
132+
// Set new timeout
133+
const timeoutId = setTimeout(async () => {
134+
await this.syncToDatabase(workspaceId, ydoc);
135+
}, this.SAVE_DELAY);
136+
137+
this.saveTimeouts.set(workspaceId, timeoutId);
138+
}
139+
140+
async syncToDatabase(workspaceId, ydoc) {
141+
try {
142+
const fileSystemMap = ydoc.getMap("fileSystem");
143+
const files = fileSystemMap.get("files") || [];
144+
145+
// Get all files (including nested ones)
146+
const getAllFiles = (fileTree) => {
147+
const allFiles = [];
148+
const traverse = (items) => {
149+
for (const item of items) {
150+
if (item.type === "file") {
151+
allFiles.push(item);
152+
} else if (item.type === "folder" && item.children) {
153+
traverse(item.children);
154+
}
155+
}
156+
};
157+
traverse(fileTree);
158+
return allFiles;
159+
};
160+
161+
// Build file path
162+
const buildFilePath = (targetFileId, fileTree, currentPath = "") => {
163+
for (const item of fileTree) {
164+
if (item.id === targetFileId && item.type === "file") {
165+
return currentPath + item.name;
166+
}
167+
if (item.type === "folder" && item.children) {
168+
const foundPath = buildFilePath(
169+
targetFileId,
170+
item.children,
171+
currentPath + item.name + "/"
172+
);
173+
if (foundPath) return foundPath;
174+
}
175+
}
176+
return null;
177+
};
178+
179+
const allFiles = getAllFiles(files);
180+
console.log(
181+
`Syncing ${allFiles.length} files for workspace ${workspaceId}`
182+
);
183+
184+
// Get current file IDs from document
185+
const currentFileIds = new Set(
186+
allFiles.map((file) => file.id).filter(Boolean)
187+
);
188+
189+
// Fetch all files for this workspace from database
190+
const { data: dbFiles, error: fetchError } = await supabase
191+
.from("files")
192+
.select("id")
193+
.eq("workspace_id", workspaceId);
194+
195+
if (fetchError) {
196+
console.error(
197+
`Error fetching files for workspace ${workspaceId}:`,
198+
fetchError
199+
);
200+
} else {
201+
// Check for files that exist in DB but not in the document - these need to be deleted
202+
for (const dbFile of dbFiles) {
203+
if (!currentFileIds.has(dbFile.id)) {
204+
// This file exists in DB but not in document, so delete it
205+
const { error: deleteError } = await supabase
206+
.from("files")
207+
.delete()
208+
.eq("id", dbFile.id);
209+
210+
if (deleteError) {
211+
console.error(`Error deleting file ${dbFile.id}:`, deleteError);
212+
} else {
213+
console.log(`🗑️ Deleted file with ID: ${dbFile.id}`);
214+
}
215+
}
216+
}
217+
}
218+
219+
// Process existing files (add/update)
220+
for (const file of allFiles) {
221+
if (!file.id) continue;
222+
223+
const fileText = ydoc.getText(`file-${file.id}`);
224+
const content = fileText.toString();
225+
const filePath = buildFilePath(file.id, files) || file.name;
226+
227+
const { data: existingFile } = await supabase
228+
.from("files")
229+
.select("id")
230+
.eq("id", file.id)
231+
.single();
232+
233+
if (!existingFile) {
234+
// Insert new file
235+
const { error } = await supabase.from("files").insert({
236+
237+
workspace_id: workspaceId,
238+
name: file.name,
239+
file_type: file.type || "file",
240+
storage_path: filePath,
241+
content: content,
242+
created_by: "4e4f8c26-6557-4578-a05c-612d1ebef6ee",
243+
});
244+
245+
if (error) {
246+
console.error(`Error inserting file ${file.id}:`, error);
247+
} else {
248+
console.log(`✅ Inserted file: ${file.name}`);
249+
}
250+
} else {
251+
// Update existing file
252+
const { error } = await supabase
253+
.from("files")
254+
.update({
255+
name: file.name,
256+
file_type: file.type || "file",
257+
storage_path: filePath,
258+
content: content,
259+
updated_at: new Date().toISOString(),
260+
})
261+
.eq("id", file.id);
262+
263+
if (error) {
264+
console.error(`Error updating file ${file.id}:`, error);
265+
} else {
266+
console.log(`✅ Updated file: ${file.name}`);
267+
}
268+
}
269+
}
270+
} catch (error) {
271+
console.error(
272+
`Failed to sync to database for workspace ${workspaceId}:`,
273+
error
274+
);
275+
}
276+
}
277+
278+
async writeState(workspaceId, ydoc) {
279+
// Clear any pending save
280+
if (this.saveTimeouts.has(workspaceId)) {
281+
clearTimeout(this.saveTimeouts.get(workspaceId));
282+
this.saveTimeouts.delete(workspaceId);
283+
}
284+
285+
// Final sync
286+
await this.syncToDatabase(workspaceId, ydoc);
287+
288+
// Cleanup
289+
this.docs.delete(workspaceId);
290+
}
291+
}
292+
293+
export const yjsPersistence = new YjsPersistence();

0 commit comments

Comments
 (0)