fix(journals): resolve browser hang and integrate Metadata component
- Implemented manual 'deep_copy' to handle reactive proxies safely.
- Added concurrency locks ('is_processing') to prevent PATCH loops.
- Standardized metadata display by switching to 'JournalEntry_Metadata'.
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
|
||||
import JournalEntry_Editor from './JournalEntry_Editor.svelte';
|
||||
import JournalEntry_Header from './JournalEntry_Header.svelte';
|
||||
import AE_MetadataFooter from '$lib/ae_elements/AE_MetadataFooter.svelte';
|
||||
import JournalEntry_Metadata from './JournalEntry_Metadata.svelte';
|
||||
import AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
|
||||
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
|
||||
|
||||
@@ -55,6 +55,32 @@
|
||||
let auto_save_timer: ReturnType<typeof setTimeout>;
|
||||
let is_processing = $state(false);
|
||||
|
||||
// *** Helpers
|
||||
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];
|
||||
if (val instanceof Date) {
|
||||
copy[key] = new Date(val.getTime());
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
} catch (e) {
|
||||
console.error('deep_copy failed:', e);
|
||||
return { ...obj };
|
||||
}
|
||||
}
|
||||
|
||||
// *** Derived
|
||||
let has_unsaved_changes = $derived.by(() => {
|
||||
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
|
||||
@@ -66,12 +92,14 @@
|
||||
const name_changed = normalize(tmp_entry_obj.name) !== normalize(orig_entry_obj.name);
|
||||
const private_changed = (tmp_entry_obj.private ?? false) !== (orig_entry_obj.private ?? false);
|
||||
|
||||
return content_changed || name_changed || private_changed ||
|
||||
(tmp_entry_obj.tags ?? null) !== (orig_entry_obj.tags ?? null) ||
|
||||
(tmp_entry_obj.category_code ?? null) !== (orig_entry_obj.category_code ?? null) ||
|
||||
(tmp_entry_obj.public ?? false) !== (orig_entry_obj.public ?? false) ||
|
||||
(tmp_entry_obj.personal ?? false) !== (orig_entry_obj.personal ?? false) ||
|
||||
(tmp_entry_obj.professional ?? false) !== (orig_entry_obj.professional ?? false);
|
||||
if (content_changed || name_changed || private_changed) return true;
|
||||
|
||||
const other_fields = ['tags', 'category_code', 'public', 'personal', 'professional'];
|
||||
for (const field of other_fields) {
|
||||
if (normalize(tmp_entry_obj[field]) !== normalize(orig_entry_obj[field])) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// *** Effects
|
||||
@@ -99,22 +127,22 @@
|
||||
}
|
||||
|
||||
console.log('ae_view: Effect[Sync] update local state.');
|
||||
const base = {
|
||||
...entry,
|
||||
content: entry.content ?? null,
|
||||
history: entry.history ?? null
|
||||
};
|
||||
orig_entry_obj = { ...base };
|
||||
tmp_entry_obj = { ...base };
|
||||
const base = deep_copy(entry);
|
||||
if (base) {
|
||||
base.content = base.content ?? null;
|
||||
base.history = base.history ?? null;
|
||||
orig_entry_obj = { ...base };
|
||||
tmp_entry_obj = { ...base };
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Auto-Save Debounce
|
||||
$effect(() => {
|
||||
// Explicitly depend on content but DO NOT react to other tmp_entry_obj properties here
|
||||
const _trigger = tmp_entry_obj.content;
|
||||
const _trigger_priv = tmp_entry_obj.private;
|
||||
// Dependency tracking
|
||||
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.');
|
||||
@@ -145,7 +173,7 @@
|
||||
const has_encrypted_content = !!$lq__journal_entry_obj?.content_encrypted;
|
||||
|
||||
if (has_encrypted_content && is_verified && decrypted_status !== true && decrypted_status !== 'processing') {
|
||||
console.log('ae_view: Effect[Decryption] auto-running.');
|
||||
console.log('ae_view: Auto-decrypt triggering.');
|
||||
run_decryption_workflow();
|
||||
}
|
||||
});
|
||||
@@ -210,9 +238,12 @@
|
||||
|
||||
async function update_journal_entry() {
|
||||
if (!$ae_loc.trusted_access || save_status === 'saving') return;
|
||||
|
||||
// Prevent concurrent saves
|
||||
if (is_processing) return;
|
||||
is_processing = true;
|
||||
save_status = 'saving';
|
||||
|
||||
// Reference Standard: Explicit whitelist of editable fields
|
||||
const data_kv: key_val = {
|
||||
name: tmp_entry_obj.name,
|
||||
content: tmp_entry_obj.content,
|
||||
@@ -236,48 +267,45 @@
|
||||
|
||||
const decrypt_key = $lq__journal_obj.combined_passcode;
|
||||
|
||||
// Encryption Logic Transition
|
||||
if (tmp_entry_obj.private) {
|
||||
if (tmp_entry_obj.content) {
|
||||
data_kv.content_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.content, decrypt_key);
|
||||
data_kv.content = null;
|
||||
}
|
||||
if (tmp_entry_obj.history) {
|
||||
data_kv.history_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.history, decrypt_key);
|
||||
data_kv.history = null;
|
||||
}
|
||||
} else {
|
||||
// Ensure we clear encrypted fields if privacy is disabled
|
||||
data_kv.content_encrypted = null;
|
||||
data_kv.history_encrypted = null;
|
||||
}
|
||||
|
||||
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,
|
||||
data_kv: data_kv,
|
||||
log_lvl: 1
|
||||
});
|
||||
// CRITICAL: Sync ORIG after save to clear unsaved changes
|
||||
orig_entry_obj = JSON.parse(JSON.stringify(tmp_entry_obj));
|
||||
|
||||
// Re-sync after successful save
|
||||
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';
|
||||
} finally {
|
||||
is_processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handle_content_decryption() {
|
||||
const journal = $lq__journal_obj;
|
||||
const entry = $lq__journal_entry_obj;
|
||||
if (!journal?.id || !entry) return;
|
||||
if (!journal?.id || !entry || is_processing) return;
|
||||
|
||||
// If encrypted and not currently decrypted, run the decryption workflow
|
||||
if (entry.content_encrypted && !tmp_entry_obj.content) {
|
||||
await run_decryption_workflow();
|
||||
} else {
|
||||
// Toggle logic: if already decrypted, "lock" it by clearing local state
|
||||
is_processing = true;
|
||||
tmp_entry_obj.content = null;
|
||||
tmp_entry_obj.history = null;
|
||||
@@ -299,7 +327,7 @@
|
||||
}
|
||||
|
||||
async function change_journal_id() {
|
||||
if (!$ae_loc.trusted_access) return;
|
||||
if (!$ae_loc.trusted_access || is_processing) return;
|
||||
await journals_func.update_ae_obj__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
||||
@@ -366,7 +394,7 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AE_MetadataFooter obj={tmp_entry_obj} />
|
||||
<JournalEntry_Metadata entry={tmp_entry_obj} />
|
||||
|
||||
<AeCompModalJournalEntryAppend
|
||||
bind:open={show_append_modal}
|
||||
@@ -383,4 +411,4 @@
|
||||
<span class="text-2xl font-bold">Loading Journal Entry...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user