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_Editor from './JournalEntry_Editor.svelte';
|
||||||
import JournalEntry_Header from './JournalEntry_Header.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 AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
|
||||||
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
|
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
|
||||||
|
|
||||||
@@ -55,6 +55,32 @@
|
|||||||
let auto_save_timer: ReturnType<typeof setTimeout>;
|
let auto_save_timer: ReturnType<typeof setTimeout>;
|
||||||
let is_processing = $state(false);
|
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
|
// *** Derived
|
||||||
let has_unsaved_changes = $derived.by(() => {
|
let has_unsaved_changes = $derived.by(() => {
|
||||||
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
|
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 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);
|
const private_changed = (tmp_entry_obj.private ?? false) !== (orig_entry_obj.private ?? false);
|
||||||
|
|
||||||
return content_changed || name_changed || private_changed ||
|
if (content_changed || name_changed || private_changed) return true;
|
||||||
(tmp_entry_obj.tags ?? null) !== (orig_entry_obj.tags ?? null) ||
|
|
||||||
(tmp_entry_obj.category_code ?? null) !== (orig_entry_obj.category_code ?? null) ||
|
const other_fields = ['tags', 'category_code', 'public', 'personal', 'professional'];
|
||||||
(tmp_entry_obj.public ?? false) !== (orig_entry_obj.public ?? false) ||
|
for (const field of other_fields) {
|
||||||
(tmp_entry_obj.personal ?? false) !== (orig_entry_obj.personal ?? false) ||
|
if (normalize(tmp_entry_obj[field]) !== normalize(orig_entry_obj[field])) return true;
|
||||||
(tmp_entry_obj.professional ?? false) !== (orig_entry_obj.professional ?? false);
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// *** Effects
|
// *** Effects
|
||||||
@@ -99,22 +127,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('ae_view: Effect[Sync] update local state.');
|
console.log('ae_view: Effect[Sync] update local state.');
|
||||||
const base = {
|
const base = deep_copy(entry);
|
||||||
...entry,
|
if (base) {
|
||||||
content: entry.content ?? null,
|
base.content = base.content ?? null;
|
||||||
history: entry.history ?? null
|
base.history = base.history ?? null;
|
||||||
};
|
|
||||||
orig_entry_obj = { ...base };
|
orig_entry_obj = { ...base };
|
||||||
tmp_entry_obj = { ...base };
|
tmp_entry_obj = { ...base };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Auto-Save Debounce
|
// 2. Auto-Save Debounce
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Explicitly depend on content but DO NOT react to other tmp_entry_obj properties here
|
// Dependency tracking
|
||||||
const _trigger = tmp_entry_obj.content;
|
const _content = tmp_entry_obj.content;
|
||||||
const _trigger_priv = tmp_entry_obj.private;
|
const _private = tmp_entry_obj.private;
|
||||||
|
|
||||||
if (has_unsaved_changes && !is_processing) {
|
if (has_unsaved_changes && !is_processing) {
|
||||||
console.log('ae_view: Effect[AutoSave] trigger.');
|
console.log('ae_view: Effect[AutoSave] trigger.');
|
||||||
@@ -145,7 +173,7 @@
|
|||||||
const has_encrypted_content = !!$lq__journal_entry_obj?.content_encrypted;
|
const has_encrypted_content = !!$lq__journal_entry_obj?.content_encrypted;
|
||||||
|
|
||||||
if (has_encrypted_content && is_verified && decrypted_status !== true && decrypted_status !== 'processing') {
|
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();
|
run_decryption_workflow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -210,9 +238,12 @@
|
|||||||
|
|
||||||
async function update_journal_entry() {
|
async function update_journal_entry() {
|
||||||
if (!$ae_loc.trusted_access || save_status === 'saving') return;
|
if (!$ae_loc.trusted_access || save_status === 'saving') return;
|
||||||
|
|
||||||
|
// Prevent concurrent saves
|
||||||
|
if (is_processing) return;
|
||||||
|
is_processing = true;
|
||||||
save_status = 'saving';
|
save_status = 'saving';
|
||||||
|
|
||||||
// Reference Standard: Explicit whitelist of editable fields
|
|
||||||
const data_kv: key_val = {
|
const data_kv: key_val = {
|
||||||
name: tmp_entry_obj.name,
|
name: tmp_entry_obj.name,
|
||||||
content: tmp_entry_obj.content,
|
content: tmp_entry_obj.content,
|
||||||
@@ -236,48 +267,45 @@
|
|||||||
|
|
||||||
const decrypt_key = $lq__journal_obj.combined_passcode;
|
const decrypt_key = $lq__journal_obj.combined_passcode;
|
||||||
|
|
||||||
// Encryption Logic Transition
|
|
||||||
if (tmp_entry_obj.private) {
|
if (tmp_entry_obj.private) {
|
||||||
if (tmp_entry_obj.content) {
|
if (tmp_entry_obj.content) {
|
||||||
data_kv.content_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.content, decrypt_key);
|
data_kv.content_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.content, decrypt_key);
|
||||||
data_kv.content = null;
|
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 {
|
} else {
|
||||||
// Ensure we clear encrypted fields if privacy is disabled
|
|
||||||
data_kv.content_encrypted = null;
|
data_kv.content_encrypted = null;
|
||||||
data_kv.history_encrypted = null;
|
data_kv.history_encrypted = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('ae_view: Starting PATCH update...');
|
||||||
await journals_func.update_ae_obj__journal_entry({
|
await journals_func.update_ae_obj__journal_entry({
|
||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
||||||
data_kv: data_kv,
|
data_kv: data_kv,
|
||||||
log_lvl: 1
|
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';
|
save_status = 'saved';
|
||||||
|
console.log('ae_view: PATCH complete and state synced.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update failed:', error);
|
console.error('Update failed:', error);
|
||||||
save_status = 'unsaved';
|
save_status = 'unsaved';
|
||||||
|
} finally {
|
||||||
|
is_processing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handle_content_decryption() {
|
async function handle_content_decryption() {
|
||||||
const journal = $lq__journal_obj;
|
const journal = $lq__journal_obj;
|
||||||
const entry = $lq__journal_entry_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) {
|
if (entry.content_encrypted && !tmp_entry_obj.content) {
|
||||||
await run_decryption_workflow();
|
await run_decryption_workflow();
|
||||||
} else {
|
} else {
|
||||||
// Toggle logic: if already decrypted, "lock" it by clearing local state
|
|
||||||
is_processing = true;
|
is_processing = true;
|
||||||
tmp_entry_obj.content = null;
|
tmp_entry_obj.content = null;
|
||||||
tmp_entry_obj.history = null;
|
tmp_entry_obj.history = null;
|
||||||
@@ -299,7 +327,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function change_journal_id() {
|
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({
|
await journals_func.update_ae_obj__journal_entry({
|
||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
||||||
@@ -366,7 +394,7 @@
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<AE_MetadataFooter obj={tmp_entry_obj} />
|
<JournalEntry_Metadata entry={tmp_entry_obj} />
|
||||||
|
|
||||||
<AeCompModalJournalEntryAppend
|
<AeCompModalJournalEntryAppend
|
||||||
bind:open={show_append_modal}
|
bind:open={show_append_modal}
|
||||||
|
|||||||
Reference in New Issue
Block a user