Line data Source code
1 : import type { Logger } from '../../platform/logger/logger.js';
2 : import type { CompositeStorageBackend } from '../../storage/composite_storage_backend.js';
3 : import { StorageWriter } from '../../storage/storage_writer.js';
4 :
5 1 : const LOG_CTX = 'session_relocation';
6 :
7 : /// Relocate a single OPFS session into a native directory, from the UI (native app only).
8 : ///
9 : /// Prompts the user for a target directory, copies just this one session's files into it, verifies
10 : /// the copy, records `location=native` in the OPFS root manifest (the registry entry stays in
11 : /// OPFS), deletes the OPFS copy of the files, and reloads the app to re-run the tested storage init
12 : /// + restore path against the new per-session layout.
13 : ///
14 : /// The OPFS copy is never deleted before the native copy is verified and the manifest is updated,
15 : /// so any failure leaves the session fully intact on OPFS.
16 : ///
17 : /// Returns false if the user cancelled the folder picker; true after a successful relocate kicks off
18 : /// the reload (the function does not return in practice on success).
19 1 : export async function relocateSessionToNative(
20 1 : sessionId: string,
21 1 : backend: CompositeStorageBackend,
22 1 : writer: StorageWriter,
23 1 : logger: Logger,
24 1 : ): Promise<boolean> {
25 : // 1. Pick a target directory. recursive:true so the picker's auto-grant also covers nested
26 : // scripts/… paths (matters even though the boot path re-grants on reload).
27 1 : const folder = await globalThis.dashqlElectron?.openDirectory('Select a folder for this session');
28 1 : if (folder == null) {
29 : // User cancelled.
30 1 : return false;
31 : }
32 :
33 1 : try {
34 : // 2. Stop the debounced writer so OPFS isn't mutated mid-copy, then drain pending writes.
35 1 : writer.pause();
36 1 : await writer.flush();
37 :
38 : // 3. Copy + verify + flip the registry + delete the OPFS copy.
39 1 : await backend.relocateSessionToNative(sessionId, folder);
40 0 : logger.info('relocated session to native storage', { sessionId, target: folder }, LOG_CTX);
41 :
42 : // 4. Reload to re-run StorageProvider init + restoreAppState against the new layout.
43 0 : globalThis.location.reload();
44 0 : return true;
45 1 : } catch (e: any) {
46 : // Re-arm the writer so the app keeps working on OPFS, then surface the failure via the toast.
47 1 : writer.resume();
48 1 : logger.error('relocating session to native storage failed', {
49 1 : sessionId,
50 1 : target: folder,
51 1 : error: (e as Error)?.message ?? String(e),
52 1 : }, LOG_CTX);
53 1 : throw e;
54 1 : }
55 1 : }
56 :
57 : /// Add a pre-existing session from a folder on disk, from the UI (native app only).
58 : ///
59 : /// Prompts the user for a folder that already holds a dashql session (e.g. one created by a previous
60 : /// run, or relocated to native storage), registers it in the OPFS root manifest as a native session,
61 : /// and reloads the app so the tested storage init + restore path brings it into the registries.
62 : ///
63 : /// Nothing is copied or written into the folder — the folder is the source of truth and stays put.
64 : /// On any failure (folder holds no session, invalid metadata, already registered) nothing is changed.
65 : ///
66 : /// Returns false if the user cancelled the folder picker; true after a successful load kicks off the
67 : /// reload (the function does not return in practice on success).
68 1 : export async function addNativeSessionFromFolder(
69 1 : backend: CompositeStorageBackend,
70 1 : logger: Logger,
71 1 : ): Promise<boolean> {
72 : // recursive:true so the picker's auto-grant also covers the session's nested scripts/… paths.
73 1 : const folder = await globalThis.dashqlElectron?.openDirectory('Open an existing session folder');
74 1 : if (folder == null) {
75 : // User cancelled.
76 1 : return false;
77 : }
78 :
79 1 : try {
80 : // Loading only touches the OPFS manifest (no per-session writes the writer could race),
81 : // so unlike relocate there's no need to pause/flush the writer here.
82 1 : const sessionId = await backend.loadNativeSession(folder);
83 0 : logger.info('added native session from folder', { sessionId, folder }, LOG_CTX);
84 :
85 : // Reload to re-run StorageProvider init + restoreAppState, which loads the new session.
86 0 : globalThis.location.reload();
87 0 : return true;
88 1 : } catch (e: any) {
89 1 : logger.error('adding native session from folder failed', {
90 1 : folder,
91 1 : error: (e as Error)?.message ?? String(e),
92 1 : }, LOG_CTX);
93 1 : throw e;
94 1 : }
95 1 : }
|