fix(journals): eliminate reactivity loops and browser hangs during privacy toggle
- Refactored all effects to use 'untrack' for rigorous dependency isolation. - Implemented 'deep_copy' helper to safely handle Svelte 5 reactive proxies. - Added concurrency locks to 'update_journal_entry' and decryption workflows. - Completed integration of 'JournalEntry_Metadata' component.
This commit is contained in:
@@ -59,7 +59,6 @@
|
||||
function deep_copy(obj: any) {
|
||||
if (!obj) return null;
|
||||
try {
|
||||
// Manual selective copy to avoid proxy issues and handle Dates
|
||||
const copy: key_val = {};
|
||||
for (const key in obj) {
|
||||
const val = obj[key];
|
||||
@@ -68,7 +67,6 @@
|
||||
} else if (Array.isArray(val)) {
|
||||
copy[key] = [...val];
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
// Simple object copy (shallow for nested to avoid recursion)
|
||||
copy[key] = { ...val };
|
||||
} else {
|
||||
copy[key] = val;
|
||||
@@ -85,7 +83,6 @@
|
||||
let has_unsaved_changes = $derived.by(() => {
|
||||
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
|
||||
|
||||
// Use normalization to avoid loop on whitespace/null differences
|
||||
const normalize = (val: any) => (val === null || val === undefined) ? '' : String(val).trim();
|
||||
|
||||
const content_changed = normalize(tmp_entry_obj.content) !== normalize(orig_entry_obj.content);
|
||||
@@ -106,27 +103,22 @@
|
||||
|
||||
// 1. Initial Load & Background Sync
|
||||
$effect(() => {
|
||||
const entry = $lq__journal_entry_obj;
|
||||
const journal = $lq__journal_obj;
|
||||
if (entry && (entry.updated_on || entry.created_on)) {
|
||||
|
||||
// ISOLATE Store checks to avoid re-triggering this effect when stores change
|
||||
const is_decrypted = untrack(() => {
|
||||
const session_kv = $journals_sess?.journal_kv[journal?.id];
|
||||
return session_kv?.journal_passcode_decrypted === true;
|
||||
});
|
||||
const entry = $lq__journal_entry_obj; // Track only entry
|
||||
|
||||
untrack(() => {
|
||||
const journal = $lq__journal_obj;
|
||||
if (!entry || !(entry.updated_on || entry.created_on)) return;
|
||||
|
||||
console.log(`ae_view: Effect[Sync] trigger. Decrypted: ${is_decrypted}. Status: ${save_status}`);
|
||||
const session_kv = $journals_sess?.journal_kv[journal?.id];
|
||||
const is_decrypted = session_kv?.journal_passcode_decrypted === true;
|
||||
|
||||
// Only sync if saved and not currently processing/editing
|
||||
if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
|
||||
// If decrypted, don't let DB's null content overwrite local recovered text
|
||||
// Prevent overwrite of recovered text if we are in a decrypted session
|
||||
if (is_decrypted && tmp_entry_obj.content && !entry.content) {
|
||||
console.log('ae_view: Effect[Sync] preserving decrypted content.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('ae_view: Effect[Sync] update local state.');
|
||||
const base = deep_copy(entry);
|
||||
if (base) {
|
||||
base.content = base.content ?? null;
|
||||
@@ -135,29 +127,31 @@
|
||||
tmp_entry_obj = { ...base };
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Auto-Save Debounce
|
||||
$effect(() => {
|
||||
// Dependency tracking
|
||||
// Track core properties
|
||||
const _content = tmp_entry_obj.content;
|
||||
const _private = tmp_entry_obj.private;
|
||||
|
||||
if (has_unsaved_changes && !is_processing) {
|
||||
console.log('ae_view: Effect[AutoSave] trigger.');
|
||||
// Isolate logic from secondary dependencies
|
||||
const should_save = untrack(() => has_unsaved_changes && !is_processing && save_status !== 'saving');
|
||||
|
||||
if (should_save) {
|
||||
if (save_status !== 'saving') save_status = 'unsaved';
|
||||
|
||||
const auto_save_enabled = untrack(() => $journals_loc.entry.auto_save);
|
||||
if (auto_save_enabled) {
|
||||
clearTimeout(auto_save_timer);
|
||||
auto_save_timer = setTimeout(() => {
|
||||
if (untrack(() => has_unsaved_changes && save_status !== 'saving')) {
|
||||
if (untrack(() => has_unsaved_changes && !is_processing && save_status !== 'saving')) {
|
||||
update_journal_entry();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
} else if (save_status !== 'saving' && !has_unsaved_changes) {
|
||||
} else if (save_status === 'unsaved' && !has_unsaved_changes) {
|
||||
save_status = 'saved';
|
||||
}
|
||||
});
|
||||
@@ -165,16 +159,17 @@
|
||||
// 3. Auto-Decryption Workflow
|
||||
$effect(() => {
|
||||
const journal = $lq__journal_obj;
|
||||
if (!journal?.id) return;
|
||||
const entry = $lq__journal_entry_obj;
|
||||
if (!journal?.id || !entry) return;
|
||||
|
||||
// Track session state selectively
|
||||
const session = $journals_sess?.journal_kv[journal.id];
|
||||
const is_verified = session?.journal_passcode_verified;
|
||||
const decrypted_status = session?.journal_passcode_decrypted;
|
||||
const has_encrypted_content = !!$lq__journal_entry_obj?.content_encrypted;
|
||||
const has_encrypted_content = !!entry.content_encrypted;
|
||||
|
||||
if (has_encrypted_content && is_verified && decrypted_status !== true && decrypted_status !== 'processing') {
|
||||
console.log('ae_view: Auto-decrypt triggering.');
|
||||
run_decryption_workflow();
|
||||
untrack(() => run_decryption_workflow());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,9 +204,6 @@
|
||||
}
|
||||
|
||||
// SUCCESS
|
||||
console.log(`ae_view: Decryption SUCCESS.`);
|
||||
|
||||
// Update both to prevent "unsaved changes" loop
|
||||
const content = result.content || '';
|
||||
tmp_entry_obj.content = content;
|
||||
tmp_entry_obj.content_md_html = handle_marked(content);
|
||||
@@ -237,10 +229,8 @@
|
||||
}
|
||||
|
||||
async function update_journal_entry() {
|
||||
if (!$ae_loc.trusted_access || save_status === 'saving') return;
|
||||
if (!$ae_loc.trusted_access || save_status === 'saving' || is_processing) return;
|
||||
|
||||
// Prevent concurrent saves
|
||||
if (is_processing) return;
|
||||
is_processing = true;
|
||||
save_status = 'saving';
|
||||
|
||||
@@ -278,7 +268,6 @@
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('ae_view: Starting PATCH update...');
|
||||
await journals_func.update_ae_obj__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
||||
@@ -286,10 +275,9 @@
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
// Re-sync after successful save
|
||||
// CRITICAL: Sync ORIG after save to clear unsaved changes
|
||||
orig_entry_obj = deep_copy(tmp_entry_obj);
|
||||
save_status = 'saved';
|
||||
console.log('ae_view: PATCH complete and state synced.');
|
||||
} catch (error) {
|
||||
console.error('Update failed:', error);
|
||||
save_status = 'unsaved';
|
||||
@@ -411,4 +399,4 @@
|
||||
<span class="text-2xl font-bold">Loading Journal Entry...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
Reference in New Issue
Block a user