Compare commits
3 Commits
535efd9c4b
...
65e48c764e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65e48c764e | ||
|
|
f6c950abdf | ||
|
|
3d6f9035c8 |
@@ -89,6 +89,88 @@ $effect(() => {
|
|||||||
|
|
||||||
- When you have chains (presentations depend on session; presenters depend on presentation.person_id), make the dependent liveQuery explicitly wait for the upstream ID and log inside each query to verify the order — adding a small `await Promise.resolve()` or `await 0` inside the `liveQuery` is sometimes useful during debugging to ensure the JS microtask queue has a chance to settle after DB writes.
|
- When you have chains (presentations depend on session; presenters depend on presentation.person_id), make the dependent liveQuery explicitly wait for the upstream ID and log inside each query to verify the order — adding a small `await Promise.resolve()` or `await 0` inside the `liveQuery` is sometimes useful during debugging to ensure the JS microtask queue has a chance to settle after DB writes.
|
||||||
|
|
||||||
|
## IDB Sort: `build_tmp_sort` Pattern (2026-05)
|
||||||
|
|
||||||
|
All Aether objects support `priority`, `sort`, `group`, and `name` fields. Rather than sorting in JS after a Dexie query (which requires `.reverse()` hacks and duplicated logic), pre-compute up to three `tmp_sort_*` string fields during the processing pipeline and store them in Dexie. Then `.sortBy('tmp_sort_2')` does the right thing in one call, with no `.reverse()`.
|
||||||
|
|
||||||
|
**Utility:** `src/lib/ae_core/core__idb_sort.ts` — `build_tmp_sort()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||||
|
|
||||||
|
// Inside specific_processor callback:
|
||||||
|
const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||||
|
prefix: [obj.group ?? '0'], // always first
|
||||||
|
priority: obj.priority, // boolean; true→'0' so ASC sorts it first
|
||||||
|
sort: obj.sort, // zero-padded to 8 chars
|
||||||
|
fields_1: [...], // module-specific tier-1 fields
|
||||||
|
fields_2: [...], // tier-2 fields (tmp_sort_2 = base + tier-1 + tier-2)
|
||||||
|
fields_3: [...] // tier-3 fields
|
||||||
|
});
|
||||||
|
obj.tmp_sort_1 = tmp_sort_1;
|
||||||
|
obj.tmp_sort_2 = tmp_sort_2;
|
||||||
|
obj.tmp_sort_3 = tmp_sort_3;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sort chain convention:** `group → priority DESC → sort ASC → [module-specific] → name`
|
||||||
|
|
||||||
|
**Priority encoding:** `priority ? '0' : '1'` — inverted so that `priority=true` sorts first in ascending order. This means **never use `.reverse()`** on a list sorted by `tmp_sort_*` — `.reverse()` would flip priority-true to sort last.
|
||||||
|
|
||||||
|
**Modules using `build_tmp_sort`:**
|
||||||
|
- `ae_events__event_presentation.ts` — `tmp_sort_1/2`: group → priority → sort → start_datetime → code → name
|
||||||
|
- `ae_journals__journal.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on
|
||||||
|
- `ae_journals__journal_entry.ts` — same chain as journal
|
||||||
|
|
||||||
|
Remaining modules (sessions, presenters, locations, posts, core) scheduled for rollout; see `TODO__Agents.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `$derived.by` Dependency Capture for Extra Filter State
|
||||||
|
|
||||||
|
When a `liveQuery` has a SCENARIO 2 fallback (broad search with no IDs), it may run before the debounced search fast path populates `event_session_id_li`. If that fallback doesn't apply the same visibility filter as the fast path, hidden items will briefly appear then disappear ("blink").
|
||||||
|
|
||||||
|
**Fix:** capture the filter flag as a `$derived.by` dependency in the outer closure so Svelte recreates the liveQuery instance whenever it changes — SCENARIO 2 then uses the correct filter from first render.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let lq__event_session_obj_li = $derived.by(() => {
|
||||||
|
const ids = event_session_id_li; // drives SCENARIO 1 vs 2
|
||||||
|
const event_id = $events_slct?.event_id;
|
||||||
|
const qry_hidden = pres_mgmt_loc.current.qry_hidden; // extra dependency
|
||||||
|
|
||||||
|
return liveQuery(async () => {
|
||||||
|
// SCENARIO 1 — specific IDs (fast path or API result)
|
||||||
|
if (Array.isArray(ids) && ids.length > 0) {
|
||||||
|
const results = await db.session.bulkGet(ids);
|
||||||
|
return results.filter(Boolean);
|
||||||
|
}
|
||||||
|
// SCENARIO 2 — broad fallback, uses captured qry_hidden
|
||||||
|
if (event_id && !someFilter) {
|
||||||
|
const all = await db.session.where('event_id').equals(event_id).sortBy('name');
|
||||||
|
return all.filter((s: any) => {
|
||||||
|
if (qry_hidden === 'not_hidden') return !s.hide;
|
||||||
|
if (qry_hidden === 'hidden') return !!s.hide;
|
||||||
|
return true; // 'all'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key rule:** anything read inside `$derived.by()`'s outer closure (but outside the `liveQuery` callback) becomes a Svelte reactive dependency. Changes to it recreate the liveQuery. Use this to synchronize filter flags that Dexie doesn't track.
|
||||||
|
|
||||||
|
**Also fix the API call:** use the snapshot value from `params` (captured at debounce time) rather than the live store, so rapid toggling doesn't create a mismatch between fast path and API results:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad — uses live store value, can race if user toggles during pending call:
|
||||||
|
hidden: pres_mgmt_loc.current.qry_hidden ?? 'not_hidden'
|
||||||
|
|
||||||
|
// Good — uses snapshot captured when handle_search_refresh was called:
|
||||||
|
hidden: params.qry_hidden ?? 'not_hidden'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings)
|
## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings)
|
||||||
- Journals: The journaling pages use SWR-style background refreshes but reliably render because either (a) the page `+page.ts` blocks to populate DB for critical views, or (b) components accept `data.initial_*` fallback values until `liveQuery` emits. This hybrid approach avoids the "refresh twice" problem while keeping navigation snappy.
|
- Journals: The journaling pages use SWR-style background refreshes but reliably render because either (a) the page `+page.ts` blocks to populate DB for critical views, or (b) components accept `data.initial_*` fallback values until `liveQuery` emits. This hybrid approach avoids the "refresh twice" problem while keeping navigation snappy.
|
||||||
- Journals broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows.
|
- Journals broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows.
|
||||||
|
|||||||
@@ -53,6 +53,21 @@ The app uses `svelte-persisted-store` (coarse reactivity). Migration target: rep
|
|||||||
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
||||||
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
||||||
|
|
||||||
|
### [IDB Sort] `build_tmp_sort` rollout
|
||||||
|
Shared utility in `src/lib/ae_core/core__idb_sort.ts` — fixes priority direction (inverted,
|
||||||
|
true→'0' sorts first ASC) and zero-pads sort field (8 chars). No `.reverse()` needed.
|
||||||
|
Sort chain: `group → priority DESC → sort ASC → [module-specific fields] → name`.
|
||||||
|
**⚠️ Never use `.reverse()` on a `tmp_sort_*`-sorted list — inverted priority makes it wrong.**
|
||||||
|
Documented in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` (IDB Sort section).
|
||||||
|
|
||||||
|
- [x] `ae_events__event_presentation` — group + priority + sort + start_datetime + code + name
|
||||||
|
- [x] `ae_journals__journal` + `ae_journals__journal_entry` — group + priority + sort + name + updated_on
|
||||||
|
- [ ] `ae_events__event_session` — roll out when sort behavior is reviewed
|
||||||
|
- [ ] `ae_events__event_presenter` — roll out when sort behavior is reviewed
|
||||||
|
- [ ] `ae_events__event_location` — roll out when sort behavior is reviewed
|
||||||
|
- [ ] `ae_posts__post` + `ae_posts__post_comment` — note: currently use 3-char padding; align to 8
|
||||||
|
- [ ] `ae_core__person` + `ae_core__account` — roll out when sort behavior is reviewed
|
||||||
|
|
||||||
### [Stores] IDB Content Version System
|
### [Stores] IDB Content Version System
|
||||||
- [x] Write `check_and_clear_idb_tables()` helper.
|
- [x] Write `check_and_clear_idb_tables()` helper.
|
||||||
- [x] Wire helper into `db_journals.ts` and IDAA layout.
|
- [x] Wire helper into `db_journals.ts` and IDAA layout.
|
||||||
@@ -69,6 +84,22 @@ The app uses `svelte-persisted-store` (coarse reactivity). Migration target: rep
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### [Pres Mgmt] Sessions hide/show toggle
|
||||||
|
- [x] **[Pres Mgmt] Hidden sessions blink on initial load** — SCENARIO 2 fallback in
|
||||||
|
`pres_mgmt/+page.svelte` now captures `qry_hidden` as a `$derived.by` dependency and
|
||||||
|
applies the filter in the fallback path. No blink on page load. (2026-05-28)
|
||||||
|
- [x] **[Pres Mgmt] API call uses live store instead of snapshot** — changed
|
||||||
|
`pres_mgmt_loc.current.qry_hidden` → `params.qry_hidden` in `handle_search_refresh`
|
||||||
|
API call to be consistent with fast path snapshot. (2026-05-28)
|
||||||
|
- **Note:** `hide_event_launcher` is still active — used in `menu_session_list.svelte`
|
||||||
|
(Launcher) to CSS-hide sessions from the list. Button to toggle it is in
|
||||||
|
`session_page_menu.svelte`. Not used in Pres Mgmt (intentional — Pres Mgmt always shows all).
|
||||||
|
- **Note:** Non-trusted users always have `!item.hide` applied at the component level
|
||||||
|
in `ae_comp__event_session_obj_li.svelte` regardless of `qry_hidden`. Toggle is
|
||||||
|
trusted-access-only in practice; direct session links still work for non-trusted users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🧪 Testing & Optimization
|
## 🧪 Testing & Optimization
|
||||||
|
|
||||||
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
|
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
|
||||||
|
|||||||
@@ -738,11 +738,6 @@ export async function search__event_session({
|
|||||||
search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
|
search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
|
||||||
else if (enabled === 'not_enabled')
|
else if (enabled === 'not_enabled')
|
||||||
search_query.and.push({ field: 'enable', op: 'eq', value: 0 });
|
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 });
|
|
||||||
|
|
||||||
if (location_name) {
|
if (location_name) {
|
||||||
search_query.and.push({
|
search_query.and.push({
|
||||||
field: 'event_location_name',
|
field: 'event_location_name',
|
||||||
@@ -774,6 +769,7 @@ export async function search__event_session({
|
|||||||
view,
|
view,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
|
hidden,
|
||||||
log_lvl
|
log_lvl
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ let last_executed_key = ''; // Search Guard Key
|
|||||||
let lq__event_session_obj_li = $derived.by(() => {
|
let lq__event_session_obj_li = $derived.by(() => {
|
||||||
const ids = event_session_id_li;
|
const ids = event_session_id_li;
|
||||||
const event_id = $events_slct?.event_id;
|
const event_id = $events_slct?.event_id;
|
||||||
|
// Captured as a $derived.by dependency so the liveQuery instance is recreated
|
||||||
|
// when qry_hidden changes — ensures SCENARIO 2 immediately reflects the new
|
||||||
|
// filter without waiting for the debounced search to set event_session_id_li.
|
||||||
|
const qry_hidden = pres_mgmt_loc.current.qry_hidden;
|
||||||
|
|
||||||
return liveQuery(async () => {
|
return liveQuery(async () => {
|
||||||
// SCENARIO 1: Specific IDs provided (Search Results)
|
// SCENARIO 1: Specific IDs provided (Search Results)
|
||||||
@@ -117,6 +121,8 @@ let lq__event_session_obj_li = $derived.by(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SCENARIO 2: Fallback broad search (Only if no active filters)
|
// SCENARIO 2: Fallback broad search (Only if no active filters)
|
||||||
|
// Applies the same hide filter as the search fast path to prevent hidden
|
||||||
|
// sessions briefly appearing on initial load before the debounce fires.
|
||||||
if (
|
if (
|
||||||
event_id &&
|
event_id &&
|
||||||
!pres_mgmt_loc.current.fulltext_search_qry_str &&
|
!pres_mgmt_loc.current.fulltext_search_qry_str &&
|
||||||
@@ -126,11 +132,16 @@ let lq__event_session_obj_li = $derived.by(() => {
|
|||||||
console.log(
|
console.log(
|
||||||
`Session Page LQ: Fallback search for event: ${event_id}`
|
`Session Page LQ: Fallback search for event: ${event_id}`
|
||||||
);
|
);
|
||||||
return await db_events.session
|
const all = await db_events.session
|
||||||
.where('event_id')
|
.where('event_id')
|
||||||
.equals(event_id)
|
.equals(event_id)
|
||||||
.limit(50)
|
.limit(50)
|
||||||
.sortBy('name');
|
.sortBy('name');
|
||||||
|
return all.filter((s: any) => {
|
||||||
|
if (qry_hidden === 'not_hidden') return !s.hide;
|
||||||
|
if (qry_hidden === 'hidden') return !!s.hide;
|
||||||
|
return true; // 'all'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@@ -310,7 +321,7 @@ async function handle_search_refresh(params: any) {
|
|||||||
ft_presentation_search_qry_str: qry_str || null,
|
ft_presentation_search_qry_str: qry_str || null,
|
||||||
location_name: location_name || null,
|
location_name: location_name || null,
|
||||||
enabled: pres_mgmt_loc.current.qry_enabled ?? 'enabled',
|
enabled: pres_mgmt_loc.current.qry_enabled ?? 'enabled',
|
||||||
hidden: pres_mgmt_loc.current.qry_hidden ?? 'not_hidden',
|
hidden: params.qry_hidden ?? 'not_hidden',
|
||||||
limit: pres_mgmt_loc.current.qry_limit__sessions ?? 100,
|
limit: pres_mgmt_loc.current.qry_limit__sessions ?? 100,
|
||||||
try_cache: true,
|
try_cache: true,
|
||||||
log_lvl: 0
|
log_lvl: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user