Skip to content
Merged
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
43 changes: 42 additions & 1 deletion src/lib/schema/fileOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { nodeRegistry } from '$lib/nodes';
import { NODE_TYPES } from '$lib/constants/nodeTypes';
import {
AUTOSAVE_KEY,
LAST_FILE_KEY,
kvDelete,
kvGet,
kvHas,
Expand Down Expand Up @@ -85,6 +86,40 @@ export function getCurrentFileName(): string | null {
export function clearCurrentFile(): void {
currentFileHandle = null;
currentFileNameStore.set(null);
void kvDelete(LAST_FILE_KEY);
}

/**
* Persist the current file reference (name + handle, when one exists) so a
* restored session keeps saving to the file the user was working on. Handles
* are structured-cloneable, so IDB stores them as-is (same mechanism as the
* recents list); writing resumes after the browser's permission re-prompt.
*/
function persistFileRef(): void {
const name = getCurrentFileName();
if (!name && !currentFileHandle) return;
void kvSet(LAST_FILE_KEY, {
name,
handle: currentFileHandle ?? undefined
}).catch(() => {});
}

/**
* Restore the persisted file reference after an autosave snapshot was loaded.
* `fallbackName` (e.g. the snapshot's metadata name) applies when no
* reference was persisted. No-op when nothing is known.
*/
export async function restoreFileRef(fallbackName?: string): Promise<void> {
try {
const ref = await kvGet<{ name: string | null; handle?: FileSystemFileHandle }>(LAST_FILE_KEY);
const name = ref?.name || fallbackName;
if (ref?.handle && hasFileSystemAccess()) {
currentFileHandle = ref.handle;
}
if (name) currentFileNameStore.set(name);
} catch {
if (fallbackName) currentFileNameStore.set(fallbackName);
}
}

/**
Expand Down Expand Up @@ -355,7 +390,9 @@ export async function installToolboxesForCurrentGraph(): Promise<void> {
*/
export async function autoSave(): Promise<void> {
try {
const file = createGraphFile('Autosave');
// Keep the working file's name in the snapshot so a restored session
// suggests it again ('Autosave' only for never-saved graphs).
const file = createGraphFile(getCurrentFileName() || 'Autosave');
await kvSet(AUTOSAVE_KEY, file);
} catch (error) {
console.warn('Autosave failed:', error);
Expand Down Expand Up @@ -412,6 +449,7 @@ export async function loadAutoSave(): Promise<boolean> {
}

await loadGraphFile(file);
await restoreFileRef(file.metadata?.name !== 'Autosave' ? file.metadata?.name : undefined);
return true;
} catch (error) {
console.warn('Failed to restore autosave, clearing:', error);
Expand Down Expand Up @@ -488,6 +526,7 @@ export async function saveAsFile(): Promise<boolean> {
// Update current file reference
currentFileHandle = handle;
currentFileNameStore.set(name);
persistFileRef();
void rememberRecent(handle);
return true;
} catch (error: any) {
Expand All @@ -514,6 +553,7 @@ function downloadGraphFile(filename: string): void {

// Set current file name for subsequent saves
currentFileNameStore.set(name);
persistFileRef();
}

/**
Expand Down Expand Up @@ -748,6 +788,7 @@ async function importModel(
componentFile.metadata.name ||
null
);
persistFileRef();
if (currentFileHandle) void rememberRecent(currentFileHandle);

return { success: true, type: 'model' };
Expand Down
3 changes: 3 additions & 0 deletions src/lib/schema/handleStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const RECENTS_STORE = 'recents';
const RECENTS_LIMIT = 10;

export const AUTOSAVE_KEY = 'autosave';
// Name + handle of the file the user is working on, persisted next to the
// autosave blob so a restored session keeps saving to the same file.
export const LAST_FILE_KEY = 'lastFile';

export interface RecentFile {
id: string;
Expand Down
7 changes: 6 additions & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { themeStore, type Theme } from '$lib/stores/theme';
import { toggleThemeWithTransition } from '$lib/utils/themeTransition';
import { AUTOSAVE_KEY, kvGet, hasFileSystemAccess, type RecentFile } from '$lib/schema/handleStore';
import { loadGraphFile, listRecentFiles, openRecentFile, removeRecentFile } from '$lib/schema/fileOps';
import { loadGraphFile, restoreFileRef, listRecentFiles, openRecentFile, removeRecentFile } from '$lib/schema/fileOps';
import type { GraphFile } from '$lib/nodes/types';
import { triggerFitView } from '$lib/stores/viewActions';
import { consoleStore } from '$lib/stores/console';
Expand Down Expand Up @@ -105,6 +105,11 @@
// Python backend) deferred; missing blocks render as
// placeholders in the preview and upgrade in the editor.
await loadGraphFile(snapshot, { deferToolboxInstall: true, backendReady: new Promise(() => {}) });
// Re-attach the session's file reference (name + handle):
// clicking through to the editor skips the restore prompt
// (graph already populated), so this is where Save gets its
// target back.
await restoreFileRef(snapshot.metadata?.name !== 'Autosave' ? snapshot.metadata?.name : undefined);
preview = 'session';
} else {
const res = await fetch(`${base}/examples/${DEFAULT_EXAMPLE.filename}`);
Expand Down
5 changes: 4 additions & 1 deletion src/routes/editor/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
import { resolveBackend } from '$lib/pyodide/backend';
import { runGraphStreamingSimulation, validateGraphSimulation, exportToPython } from '$lib/pyodide/pathsimRunner';
import { consoleStore } from '$lib/stores/console';
import { newGraph, saveFile, saveAsFile, setupAutoSave, clearAutoSave, debouncedAutoSave, openImportDialog, importFromUrl, currentFileName, loadGraphFile, listRecentFiles, openRecentFile, removeRecentFile, installToolboxesForCurrentGraph } from '$lib/schema/fileOps';
import { newGraph, saveFile, saveAsFile, setupAutoSave, clearAutoSave, debouncedAutoSave, openImportDialog, importFromUrl, currentFileName, loadGraphFile, restoreFileRef, listRecentFiles, openRecentFile, removeRecentFile, installToolboxesForCurrentGraph } from '$lib/schema/fileOps';
import { AUTOSAVE_KEY, kvGet, hasFileSystemAccess, type RecentFile } from '$lib/schema/handleStore';
import type { GraphFile } from '$lib/nodes/types';
import { confirmationStore } from '$lib/stores/confirmation';
Expand Down Expand Up @@ -730,6 +730,9 @@
if (ok) {
try {
await loadGraphFile(snapshot);
// Re-attach the file the session was working on (name +
// handle), so Save targets it again instead of a default.
await restoreFileRef(snapshot.metadata?.name !== 'Autosave' ? snapshot.metadata?.name : undefined);
setTimeout(() => triggerFitView(), 100);
} catch (e) {
console.warn('Failed to restore autosave:', e);
Expand Down