Files
OSIT-AE-App-Svelte/src/lib/ae_events/ae_events__event_presentation.ts
Scott Idem 4a39ca1468 refactor(sort): introduce build_tmp_sort utility; apply to event_presentation and journals
Creates src/lib/ae_core/core__idb_sort.ts with build_tmp_sort() — a shared
helper for computing tmp_sort_1/2/3 fields stored in Dexie. Fixes two bugs
present in all generic _process_generic_props implementations:
  - priority encoded as 0/1 ASC (true sorted last); now inverted: true→'0'
  - sort stored as unpadded string ("10" < "2"); now 8-char zero-padded

Applied to:
  - ae_events__event_presentation: replaces inline specific_processor code
  - ae_journals__journal + ae_journals__journal_entry: replaces manual formulas;
    journal liveQueries (.reverse().sortBy()) updated to plain .sortBy() since
    the inverted encoding handles direction without needing Dexie's .reverse()

Other modules (sessions, presenters, locations, posts, core) left unchanged
until their sort behavior is reviewed separately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:38:25 -04:00

699 lines
20 KiB
TypeScript

import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_events } from '$lib/ae_events/db_events';
import type { ae_EventPresentation } from '$lib/types/ae_types';
import { load_ae_obj_li__event_file } from '$lib/ae_events/ae_events__event_file';
import { load_ae_obj_li__event_presenter } from '$lib/ae_events/ae_events__event_presenter';
const ae_promises: key_val = {};
// Updated 2026-01-30: Optimized SWR Pattern
export async function load_ae_obj_id__event_presentation({
api_cfg,
event_presentation_id,
inc_file_li = false,
inc_presenter_li = false,
enabled = 'enabled',
hidden = 'not_hidden',
view = 'default',
limit = 100,
offset = 0,
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
event_presentation_id: string;
inc_file_li?: boolean;
inc_presenter_li?: boolean;
enabled?: 'enabled' | 'all' | 'not_enabled';
hidden?: 'hidden' | 'all' | 'not_hidden';
view?: string;
limit?: number;
offset?: number;
try_cache?: boolean;
log_lvl?: number;
}): Promise<ae_EventPresentation | null> {
if (log_lvl) {
console.log(
`*** load_ae_obj_id__event_presentation() *** id=${event_presentation_id} (SWR)`
);
}
// 1. FAST PATH: Cache hit
if (try_cache) {
try {
const cached = await db_events.presentation.get(
event_presentation_id
);
if (cached) {
// Background refresh (non-blocking)
_refresh_presentation_id_background({
api_cfg,
event_presentation_id,
view,
try_cache,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
log_lvl: 0
});
// Await nested loads from cache to return a complete object
return await _handle_nested_loads(cached, {
api_cfg,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
try_cache,
log_lvl: 0
});
}
} catch (e) {}
}
// 2. SLOW PATH: Wait for API
return await _refresh_presentation_id_background({
api_cfg,
event_presentation_id,
view,
try_cache,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
log_lvl
});
}
/**
* Internal background refresh for a single presentation
*/
async function _refresh_presentation_id_background({
api_cfg,
event_presentation_id,
view,
try_cache,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
log_lvl
}: any) {
if (typeof navigator !== 'undefined' && !navigator.onLine) return null;
try {
const result = await api.get_ae_obj({
api_cfg,
obj_type: 'event_presentation',
obj_id: event_presentation_id,
view,
log_lvl
});
if (result) {
const processed = await process_ae_obj__event_presentation_props({
obj_li: [result],
log_lvl
});
const processed_obj = processed[0];
if (try_cache) {
await db_save_ae_obj_li__ae_obj({
db_instance: db_events,
table_name: 'presentation',
obj_li: [processed_obj],
properties_to_save,
log_lvl
});
}
// During refresh, we disable child SWR to prevent a request storm
return await _handle_nested_loads(processed_obj, {
api_cfg,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
try_cache: false,
log_lvl
});
}
} catch (e) {}
return null;
}
/**
* Helper to handle nested collection loads for a presentation
*/
async function _handle_nested_loads(
presentation_obj: any,
{
api_cfg,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
try_cache,
log_lvl
}: any
) {
const current_presentation_id =
presentation_obj.id || presentation_obj.event_presentation_id;
if (!current_presentation_id) return presentation_obj;
const tasks = [];
if (inc_file_li) {
tasks.push(
load_ae_obj_li__event_file({
api_cfg,
for_obj_type: 'event_presentation',
for_obj_id: current_presentation_id,
enabled,
// WHY: include hidden files so Manage Files UI can show/unhide them.
hidden: 'all',
limit: 25,
try_cache,
log_lvl
}).then((res) => (presentation_obj.event_file_li = res))
);
}
if (inc_presenter_li) {
tasks.push(
load_ae_obj_li__event_presenter({
api_cfg,
for_obj_type: 'event_presentation',
for_obj_id: current_presentation_id,
inc_file_li,
enabled,
hidden,
limit,
offset,
try_cache,
log_lvl
}).then((res) => (presentation_obj.event_presenter_li = res))
);
}
if (tasks.length > 0) await Promise.all(tasks);
return presentation_obj;
}
// Updated 2026-01-30: Parallelized Nested Loads
export async function load_ae_obj_li__event_presentation({
api_cfg,
for_obj_type = 'event_session',
for_obj_id,
inc_file_li = false,
inc_presenter_li = false,
enabled = 'enabled',
hidden = 'not_hidden',
view = 'default',
limit = 50,
offset = 0,
order_by_li = [
{ priority: 'DESC' },
{ sort: 'DESC' },
{ start_datetime: 'ASC' },
{ name: 'ASC' },
{ updated_on: 'DESC' }
],
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
for_obj_type?: string;
for_obj_id: string;
inc_file_li?: boolean;
inc_presenter_li?: boolean;
enabled?: 'enabled' | 'all' | 'not_enabled';
hidden?: 'hidden' | 'all' | 'not_hidden';
view?: string;
limit?: number;
offset?: number;
order_by_li?: any;
try_cache?: boolean;
log_lvl?: number;
}): Promise<ae_EventPresentation[]> {
if (log_lvl) {
console.log(
`*** load_ae_obj_li__event_presentation() *** for=${for_obj_type}:${for_obj_id} (SWR)`
);
}
// 1. FAST PATH: Cache hit
if (try_cache) {
try {
// String-Only ID Vision: query event_session_id using the string ID
const cached_li = await db_events.presentation
.where('event_session_id')
.equals(for_obj_id)
.toArray();
if (cached_li && cached_li.length > 0) {
// Background refresh (non-blocking)
_refresh_presentation_li_background({
api_cfg,
for_obj_type,
for_obj_id,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
view,
limit,
offset,
order_by_li,
try_cache,
log_lvl: 0
});
// Warm cache for nested loads in the background (FIRE AND FORGET)
// DEPRECATED Optimization: Don't fire child loads for every item in a list here.
// Let the specific Presentation component handle its own children to stagger requests.
/*
cached_li.forEach(p => {
_handle_nested_loads(p, { api_cfg, inc_file_li, inc_presenter_li, enabled, hidden, limit, offset, try_cache, log_lvl: 0 });
});
*/
return cached_li;
}
} catch (e) {}
}
// 2. SLOW PATH: Wait for API
return await _refresh_presentation_li_background({
api_cfg,
for_obj_type,
for_obj_id,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
view,
limit,
offset,
order_by_li,
try_cache,
log_lvl
});
}
async function _refresh_presentation_li_background({
api_cfg,
for_obj_type,
for_obj_id,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
view,
limit,
offset,
order_by_li,
try_cache,
log_lvl
}: any) {
if (typeof navigator !== 'undefined' && !navigator.onLine) return [];
try {
const result_li = await api.get_ae_obj_li({
api_cfg,
obj_type: 'event_presentation',
for_obj_type,
for_obj_id,
enabled,
hidden,
view,
limit,
offset,
order_by_li,
log_lvl
});
if (result_li) {
const processed = await process_ae_obj__event_presentation_props({
obj_li: result_li,
log_lvl
});
// Ensure the linking ID is set correctly for indexing
if (for_obj_type === 'event_session') {
processed.forEach((p) => (p.event_session_id = for_obj_id));
}
if (try_cache) {
await db_save_ae_obj_li__ae_obj({
db_instance: db_events,
table_name: 'presentation',
obj_li: processed,
properties_to_save,
log_lvl
});
// CRITICAL FIX (2026-02-26): Yield to microtask queue so Dexie liveQuery observers
// fire before we return. Without this, component-mounted liveQueries may subscribe
// to IDB *before* the write completes, causing empty results on cold-start.
await Promise.resolve();
}
// CRITICAL FIX (2026-02-26): Block on nested loads when explicitly requested.
// Previously fire-and-forget (forEach without await), which meant the function returned
// before presenter data was loaded, causing "refresh twice" bug on cold-start.
// Now we await all nested loads AND preserve try_cache so presenters are written to IDB.
if (inc_file_li || inc_presenter_li) {
await Promise.all(
processed.map((p) =>
_handle_nested_loads(p, {
api_cfg,
inc_file_li,
inc_presenter_li,
enabled,
hidden,
limit,
offset,
try_cache,
log_lvl: 0
})
)
);
}
return processed;
}
} catch (e) {}
return [];
}
// Updated 2026-01-20 to V3
export async function create_ae_obj__event_presentation({
api_cfg,
event_session_id,
data_kv,
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
event_session_id: string;
data_kv: key_val;
try_cache?: boolean;
log_lvl?: number;
}): Promise<ae_EventPresentation | null> {
const result = await api.create_nested_obj({
api_cfg,
for_obj_type: 'event_session',
for_obj_id: event_session_id,
obj_type: 'event_presentation',
fields: { ...data_kv },
log_lvl
});
if (result) {
const processed = await process_ae_obj__event_presentation_props({
obj_li: [result],
log_lvl
});
const processed_obj = processed[0];
if (try_cache) {
await db_save_ae_obj_li__ae_obj({
db_instance: db_events,
table_name: 'presentation',
obj_li: [processed_obj],
properties_to_save,
log_lvl
});
}
return processed_obj;
}
return null;
}
// Updated 2026-01-20 to V3
export async function delete_ae_obj_id__event_presentation({
api_cfg,
event_presentation_id,
method = 'delete',
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
event_presentation_id: string;
method?: 'delete' | 'soft_delete' | 'disable' | 'hide';
try_cache?: boolean;
log_lvl?: number;
}) {
const result = await api.delete_ae_obj({
api_cfg,
obj_type: 'event_presentation',
obj_id: event_presentation_id,
method,
log_lvl
});
if (try_cache) await db_events.presentation.delete(event_presentation_id);
return result;
}
// Updated 2026-01-20 to V3
export async function update_ae_obj__event_presentation({
api_cfg,
event_presentation_id,
data_kv,
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
event_presentation_id: string;
data_kv: key_val;
try_cache?: boolean;
log_lvl?: number;
}): Promise<ae_EventPresentation | null> {
const result = await api.update_ae_obj({
api_cfg,
obj_type: 'event_presentation',
obj_id: event_presentation_id,
fields: data_kv,
log_lvl
});
if (result) {
const processed = await process_ae_obj__event_presentation_props({
obj_li: [result],
log_lvl
});
const processed_obj = processed[0];
if (try_cache) {
await db_save_ae_obj_li__ae_obj({
db_instance: db_events,
table_name: 'presentation',
obj_li: [processed_obj],
properties_to_save,
log_lvl
});
}
return processed_obj;
}
return null;
}
// Updated 2026-01-21 to Restore Full Aether Search Logic
export async function search__event_presentation({
api_cfg,
event_id,
fulltext_search_qry_str = '',
like_search_qry_str = '',
enabled = 'enabled',
hidden = 'not_hidden',
view = 'default',
limit = 50,
offset = 0,
order_by_li = [{ sort: 'ASC' }, { start_datetime: 'ASC' }, { name: 'ASC' }],
try_cache = true,
log_lvl = 0
}: {
api_cfg: any;
event_id: string;
fulltext_search_qry_str?: string;
like_search_qry_str?: string;
enabled?: 'enabled' | 'all' | 'not_enabled';
hidden?: 'hidden' | 'all' | 'not_hidden';
view?: string;
limit?: number;
offset?: number;
order_by_li?: any;
try_cache?: boolean;
log_lvl?: number;
}): Promise<ae_EventPresentation[]> {
const search_query: any = {
q: '',
and: [{ field: 'event_id', op: 'eq', value: event_id }]
};
const params: key_val = {};
if (fulltext_search_qry_str && fulltext_search_qry_str.length > 2)
params['ft_qry'] = { default_qry_str: fulltext_search_qry_str };
if (like_search_qry_str)
params['lk_qry'] = { default_qry_str: like_search_qry_str };
if (enabled === 'enabled')
search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
else if (enabled === 'not_enabled')
search_query.and.push({ field: 'enable', op: 'eq', value: 0 });
if (hidden === 'hidden')
search_query.and.push({ field: 'hide', op: 'eq', value: 1 });
else if (hidden === 'not_hidden')
search_query.and.push({ field: 'hide', op: 'eq', value: 0 });
const result_li = await api.search_ae_obj({
api_cfg,
obj_type: 'event_presentation',
search_query,
order_by_li,
params,
view,
limit,
offset,
log_lvl
});
if (result_li) {
const processed = await process_ae_obj__event_presentation_props({
obj_li: result_li,
log_lvl
});
if (try_cache) {
await db_save_ae_obj_li__ae_obj({
db_instance: db_events,
table_name: 'presentation',
obj_li: processed,
properties_to_save,
log_lvl
});
}
return processed;
}
return [];
}
export const qry__event_presentation = search__event_presentation;
export const properties_to_save = [
'id',
'event_presentation_id',
'event_presentation_id_random',
'external_id',
'code',
'for_type',
'for_id',
'for_id_random',
'type_code',
'event_id',
'event_session_id',
'event_abstract_id',
'event_id_random',
'event_session_id_random',
'event_abstract_id_random',
'abstract_code',
'name',
'description',
'start_datetime',
'end_datetime',
'passcode',
'hide_event_launcher',
'enable',
'hide',
'priority',
'sort',
'group',
'notes',
'created_on',
'updated_on',
'tmp_sort_1',
'tmp_sort_2',
'event_session_code',
'event_session_name'
];
async function _process_generic_props<T extends Record<string, any>>({
obj_li,
obj_type,
log_lvl = 0,
specific_processor
}: {
obj_li: T[];
obj_type: string;
log_lvl?: number;
specific_processor?: (obj: T) => Promise<T> | T;
}): Promise<T[]> {
if (!obj_li || obj_li.length === 0) return [];
const processed_obj_li: T[] = [];
for (const original_obj of obj_li) {
let processed_obj = { ...original_obj };
for (const key in processed_obj) {
if (key.endsWith('_random')) {
const newKey = key.slice(0, -7);
(processed_obj as any)[newKey] = processed_obj[key];
}
}
const randomIdKey = `${obj_type}_id_random`;
const baseIdKey = `${obj_type}_id`;
if (processed_obj[randomIdKey])
(processed_obj as any).id = processed_obj[randomIdKey];
else if (processed_obj[baseIdKey])
(processed_obj as any).id = processed_obj[baseIdKey];
const group = processed_obj.group ?? '0';
const priority = processed_obj.priority ? 1 : 0;
const sort = processed_obj.sort ?? '0';
const updated =
processed_obj.updated_on ??
processed_obj.created_on ??
new Date(0).toISOString();
const name = processed_obj.name ?? '';
(processed_obj as any).tmp_sort_1 =
`${group}_${priority}_${sort}_${updated}`;
(processed_obj as any).tmp_sort_2 =
`${group}_${priority}_${sort}_${name}_${updated}`;
if (specific_processor)
processed_obj = await Promise.resolve(
specific_processor(processed_obj)
);
processed_obj_li.push(processed_obj as T);
}
return processed_obj_li;
}
export async function process_ae_obj__event_presentation_props({
obj_li,
log_lvl = 0
}: {
obj_li: any[];
log_lvl?: number;
}) {
return _process_generic_props({
obj_li,
obj_type: 'event_presentation',
log_lvl,
specific_processor: (obj) => {
// Ensure linking IDs are the string versions for indexing
if (obj.event_session_id_random)
obj.event_session_id = obj.event_session_id_random;
if (obj.event_id_random) obj.event_id = obj.event_id_random;
// Override generic tmp_sort_* with presentation-specific encoding via
// build_tmp_sort. Order: priority DESC → sort ASC → start_datetime ASC → code ASC → name ASC
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
priority: obj.priority,
sort: obj.sort,
fields_1: [obj.start_datetime, obj.code],
fields_2: [obj.name]
});
obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
return obj;
}
});
}