refactor(idaa): migrate favorites storage from mod_meetings_json to data_store
Favorites are now stored in a dedicated data_store record (code:
idaa_meetings_favorites, scoped to the IDAA account_id) so toggling
never touches ae_event rows or their updated_on timestamps. Structure:
{ [novi_uuid]: [event_id, ...] }. Pre-created DB records for dev (id 150)
and live IDAA (id 151) accounts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// *** Import Svelte specific
|
// *** Import Svelte specific
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
// *** Import Aether specific variables and functions
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
@@ -37,54 +38,81 @@ import {
|
|||||||
idaa_slct,
|
idaa_slct,
|
||||||
idaa_trig
|
idaa_trig
|
||||||
} from '$lib/stores/ae_idaa_stores';
|
} from '$lib/stores/ae_idaa_stores';
|
||||||
import { db_events } from '$lib/ae_events/db_events';
|
import { api } from '$lib/api/api';
|
||||||
import { events_func } from '$lib/ae_events/ae_events_functions';
|
|
||||||
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
||||||
|
|
||||||
|
// Favorites are stored in a dedicated data_store record (code: idaa_meetings_favorites,
|
||||||
|
// scoped to the IDAA account_id) so toggling never touches ae_event rows or their
|
||||||
|
// updated_on timestamps. Structure: { [novi_uuid]: [event_id, ...] }.
|
||||||
|
// One shared record per account — known race condition if two members toggle at
|
||||||
|
// the exact same moment (last write wins). Acceptable for ~1000 members.
|
||||||
|
let ds_fav_id = $state<string | null>(null);
|
||||||
|
let ds_fav_json = $state<Record<string, string[]>>({});
|
||||||
|
|
||||||
// Tracks event IDs currently being toggled to prevent double-clicks
|
// Tracks event IDs currently being toggled to prevent double-clicks
|
||||||
let favorites_in_progress = $state<Set<string>>(new Set());
|
let favorites_in_progress = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$idaa_loc.novi_uuid || !$ae_api?.base_url) return;
|
||||||
|
try {
|
||||||
|
const result = await api.get_data_store({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
code: 'idaa_meetings_favorites'
|
||||||
|
});
|
||||||
|
const rec_id = result?.data_store_id || result?.id;
|
||||||
|
if (rec_id) {
|
||||||
|
ds_fav_id = rec_id;
|
||||||
|
let raw = result.json ?? result.json_str ?? null;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try { raw = JSON.parse(raw); } catch { raw = {}; }
|
||||||
|
}
|
||||||
|
ds_fav_json = (raw as Record<string, string[]>) ?? {};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[favorites] Failed to load favorites data store:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function check_fav(event_obj: Record<string, unknown>): boolean {
|
function check_fav(event_obj: Record<string, unknown>): boolean {
|
||||||
const my_uuid = $idaa_loc.novi_uuid;
|
const my_uuid = $idaa_loc.novi_uuid;
|
||||||
if (!my_uuid) return false;
|
if (!my_uuid) return false;
|
||||||
const mod = event_obj.mod_meetings_json as Record<string, unknown> | null | undefined;
|
const my_favs = ds_fav_json[my_uuid];
|
||||||
return Array.isArray(mod?.favorite) && (mod.favorite as string[]).includes(my_uuid);
|
return Array.isArray(my_favs) && my_favs.includes(event_obj.event_id as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggle_favorite(event_obj: Record<string, unknown>) {
|
async function toggle_favorite(event_obj: Record<string, unknown>) {
|
||||||
const my_uuid = $idaa_loc.novi_uuid;
|
const my_uuid = $idaa_loc.novi_uuid;
|
||||||
const obj_id = event_obj.id as string; // Dexie primary key (same value as event_id)
|
const event_id = event_obj.event_id as string;
|
||||||
const event_id = event_obj.event_id as string; // V3 API UUID
|
|
||||||
|
|
||||||
if (!my_uuid || !event_id || favorites_in_progress.has(event_id)) return;
|
if (!my_uuid || !event_id || !ds_fav_id || favorites_in_progress.has(event_id)) return;
|
||||||
|
|
||||||
favorites_in_progress.add(event_id);
|
favorites_in_progress.add(event_id);
|
||||||
|
|
||||||
const current_json = (event_obj.mod_meetings_json as Record<string, unknown>) ?? {};
|
const current_user_list: string[] = Array.isArray(ds_fav_json[my_uuid])
|
||||||
const current_list: string[] = Array.isArray(current_json.favorite) ? [...(current_json.favorite as string[])] : [];
|
? [...ds_fav_json[my_uuid]]
|
||||||
const already_fav = current_list.includes(my_uuid);
|
: [];
|
||||||
const new_list = already_fav ? current_list.filter((u) => u !== my_uuid) : [...current_list, my_uuid];
|
const already_fav = current_user_list.includes(event_id);
|
||||||
const new_json = { ...current_json, favorite: new_list };
|
const new_user_list = already_fav
|
||||||
|
? current_user_list.filter((id) => id !== event_id)
|
||||||
|
: [...current_user_list, event_id];
|
||||||
|
|
||||||
// Optimistic IDB update — liveQuery reactively re-renders the card instantly
|
const prev_json = ds_fav_json;
|
||||||
await db_events.event.update(obj_id, { mod_meetings_json: new_json });
|
const new_full_json = { ...ds_fav_json, [my_uuid]: new_user_list };
|
||||||
|
|
||||||
|
// Optimistic update — ds_fav_json is reactive $state, so check_fav() re-evaluates
|
||||||
|
// instantly everywhere it's called (derived list sort + each block template).
|
||||||
|
ds_fav_json = new_full_json;
|
||||||
|
|
||||||
// NOTE: This PATCH triggers ae_event.updated_on to update (MariaDB ON UPDATE
|
|
||||||
// current_timestamp() fires on every row write). Side effect: starring a meeting
|
|
||||||
// shifts its position when sorted by updated_on, and admins see a spurious "last modified"
|
|
||||||
// timestamp. Long-term fix: either bypass ON UPDATE for this specific write (pass
|
|
||||||
// updated_on = updated_on in the SET clause server-side), or store favorites in a
|
|
||||||
// dedicated per-user table that doesn't touch the event row at all.
|
|
||||||
try {
|
try {
|
||||||
await events_func.update_ae_obj__event({
|
await api.update_ae_obj({
|
||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
event_id,
|
obj_type: 'data_store',
|
||||||
data_kv: { mod_meetings_json: new_json },
|
obj_id: ds_fav_id,
|
||||||
try_cache: true
|
fields: { json: JSON.stringify(new_full_json) }
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[favorites] API persist failed, rolling back:', err);
|
console.warn('[favorites] API persist failed, rolling back:', err);
|
||||||
await db_events.event.update(obj_id, { mod_meetings_json: current_json });
|
ds_fav_json = prev_json;
|
||||||
} finally {
|
} finally {
|
||||||
favorites_in_progress.delete(event_id);
|
favorites_in_progress.delete(event_id);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user