From 2306f2d0c42327951e9729240382cd92c5fa98e0 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 5 Feb 2026 12:27:26 -0500 Subject: [PATCH] fix(idaa): harden data sync against padStart crashes and fix Archive Content sort - Hardened object processors in Archives and Posts modules to safely handle null 'sort' values, preventing runtime TypeErrors during data synchronization. - Fixed inconsistent sorting in Archive Content list by correctly implementing descending order (sort then reverse) and adding a configuration loading guard to the liveQuery. - Standardized safe data processing patterns in SVELTE_DEXIE_GUIDE.md. - Performed minor cleanup and visibility logic hardening in Recovery Meetings module. --- documentation/SVELTE_DEXIE_GUIDE.md | 38 +++++++- src/lib/ae_archives/ae_archives__archive.ts | 5 +- .../ae_archives__archive_content.ts | 7 +- src/lib/ae_posts/ae_posts__post.ts | 5 +- src/lib/ae_posts/ae_posts__post_comment.ts | 5 +- .../(idaa)/archives/[archive_id]/+page.svelte | 4 +- .../ae_idaa_comp__event_obj_id_edit.svelte | 1 + .../ae_idaa_comp__event_obj_li.svelte | 96 +++++++++---------- .../ae_idaa_comp__event_obj_qry.svelte | 6 +- 9 files changed, 104 insertions(+), 63 deletions(-) diff --git a/documentation/SVELTE_DEXIE_GUIDE.md b/documentation/SVELTE_DEXIE_GUIDE.md index d423ed1d..1f52e35e 100644 --- a/documentation/SVELTE_DEXIE_GUIDE.md +++ b/documentation/SVELTE_DEXIE_GUIDE.md @@ -92,6 +92,42 @@ export function createLiveQueryStore(query: () => T | Promise) { The `createLiveQueryStore` function creates a readable store that automatically updates whenever the data in the `friends` table changes. The `$friends` variable in the component will always contain the latest data from the database. +## Safe Data Processing for IndexedDB Sorting + +When preparing data for IndexedDB, especially when creating composite sort keys, it is critical to handle `null` or `undefined` values safely to prevent runtime crashes that can interrupt the data synchronization process. + +### 1. Safe String Padding +Attempting to call `.toString()` or `.padStart()` on a `null` or `undefined` value will throw a `TypeError`. This is a common pitfall when processing optional fields like `sort` or `group`. + +**Bad Pattern (Crash Risk):** +```typescript +// Crashes if obj.sort is null or undefined +obj.tmp_sort_1 = `${obj.sort.toString().padStart(3, '0')}`; +obj.tmp_sort_2 = `${obj.sort?.toString().padStart(3, '0') ?? ''}`; // Still risky if chaining is misunderstood +``` + +**Good Pattern (Safe):** +```typescript +// Safely handle null/undefined by defaulting to 0 or an empty string BEFORE string manipulation +const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); +``` + +### 2. Correct Sorting with Dexie +Dexie's `sortBy()` method returns a new array sorted by the specified key. It **ignores** previous `reverse()` calls on the collection. To achieve a descending sort, you must sort first and then reverse the resulting array. + +**Incorrect (Ascending Sort Result):** +```typescript +// .reverse() is ignored by .sortBy() +let results = await db.table.where('id').equals(id).reverse().sortBy('sort_key'); +``` + +**Correct (Descending Sort Result):** +```typescript +// Sort ascending first, then reverse the array +let results = await db.table.where('id').equals(id).sortBy('sort_key'); +return results.reverse(); +``` + ## Current Data Flow in `ae_journals` Module The `ae_journals` module currently uses a manual, API-first caching strategy. It does not use Dexie.js's `liveQuery` function for reactivity. @@ -105,4 +141,4 @@ The `ae_journals` module currently uses a manual, API-first caching strategy. It ### Future Improvements -The current implementation could be improved by refactoring it to use Dexie.js's `liveQuery` function. This would simplify the code, reduce boilerplate, and improve the reactivity of the application. By using `liveQuery`, the UI would automatically update whenever the data in the IndexedDB database changes, without the need for manual store updates. +The current implementation could be improved by refactoring it to use Dexie.js's `liveQuery` function. This would simplify the code, reduce boilerplate, and improve the reactivity of the application. By using `liveQuery`, the UI would automatically update whenever the data in the IndexedDB database changes, without the need for manual store updates. \ No newline at end of file diff --git a/src/lib/ae_archives/ae_archives__archive.ts b/src/lib/ae_archives/ae_archives__archive.ts index ae40e0ed..f0e70b22 100644 --- a/src/lib/ae_archives/ae_archives__archive.ts +++ b/src/lib/ae_archives/ae_archives__archive.ts @@ -472,11 +472,12 @@ export async function process_ae_obj__archive_props({ obj_type: 'archive', log_lvl, specific_processor: (obj) => { + const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(3, '0') ?? '' + sort_val }_${obj.updated_on ?? obj.created_on}`; obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(3, '0') ?? '' + sort_val }_${obj.updated_on}_${obj.created_on}`; return obj; diff --git a/src/lib/ae_archives/ae_archives__archive_content.ts b/src/lib/ae_archives/ae_archives__archive_content.ts index fc9d4fd6..fe9084ae 100644 --- a/src/lib/ae_archives/ae_archives__archive_content.ts +++ b/src/lib/ae_archives/ae_archives__archive_content.ts @@ -384,15 +384,16 @@ export async function process_ae_obj__archive_content_props({ obj_type: 'archive_content', log_lvl, specific_processor: (obj) => { + const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(3, '0') ?? '' + sort_val }_${obj.original_datetime ?? ''}`; obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.original_datetime ?? ''}_${ obj.priority ? '1' : '0' - }_${obj.sort?.toString().padStart(3, '0') ?? ''}`; + }_${sort_val}`; obj.tmp_sort_3 = `${obj.original_datetime ?? ''}_${obj.group ?? ''}_${ obj.priority ? '1' : '0' - }_${obj.sort?.toString().padStart(3, '0') ?? ''}`; + }_${sort_val}`; obj.hash_sha256 = obj.hosted_file_hash_sha256; diff --git a/src/lib/ae_posts/ae_posts__post.ts b/src/lib/ae_posts/ae_posts__post.ts index 66dfea81..1d826aef 100644 --- a/src/lib/ae_posts/ae_posts__post.ts +++ b/src/lib/ae_posts/ae_posts__post.ts @@ -548,11 +548,12 @@ export async function process_ae_obj__post_props({ if (!obj.account_id_random) obj.account_id_random = account_id; } obj.name = obj.title; + const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(3, '0') ?? '' + sort_val }_${obj.updated_on ?? obj.created_on}`; obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(3, '0') ?? '' + sort_val }_${obj.updated_on}_${obj.created_on}`; return obj; diff --git a/src/lib/ae_posts/ae_posts__post_comment.ts b/src/lib/ae_posts/ae_posts__post_comment.ts index 9749fbb3..79fa1fbb 100644 --- a/src/lib/ae_posts/ae_posts__post_comment.ts +++ b/src/lib/ae_posts/ae_posts__post_comment.ts @@ -360,11 +360,12 @@ export async function process_ae_obj__post_comment_props({ obj_type: 'post_comment', log_lvl, specific_processor: (obj) => { + const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(3, '0') ?? '' + sort_val }_${obj.updated_on ?? obj.created_on}`; obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ - obj.sort?.toString().padStart(2, '0') ?? '' + (obj.sort ?? 0).toString().padStart(2, '0') }_${obj.updated_on}_${obj.created_on}`; return obj; diff --git a/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte b/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte index 566b5aab..628d2e5e 100644 --- a/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte +++ b/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte @@ -99,11 +99,11 @@ let results = await db_archives.content .where('archive_id') .equals($idaa_slct?.archive_id ?? '') // null or undefined does not reset things like '' does - .reverse() + // .reverse() // Incorrect usage: reverse() on collection is ignored by sortBy() .sortBy('tmp_sort_2'); // .sortBy('updated_on'); - return results; + return results.reverse(); } else { let results = await db_archives.content .where('archive_id') diff --git a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte index 7def517c..d3950106 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte @@ -1974,6 +1974,7 @@ preset-tonal-surface hover:preset-filled-surface-100-900 form-control col-6 " + class:required={!$lq__event_obj?.timezone && !$ae_loc?.current_timezone} > { // Subscribe to the LiveQuery observable using $ prefix const list = $lq__event_obj_li; - + if (!list || !Array.isArray(list)) { if (log_lvl > 1) console.log('visible_event_obj_li: Waiting for data stream...'); return []; } - + const filtered = list.filter((item: any) => { if (!item) return false; - + // ADMIN/TRUSTED: See everything if ($ae_loc.trusted_access) return true; - + // PUBLIC: Filter hidden/disabled // Safely handle null/undefined fields by assuming visible/enabled (permissive default) const is_hidden = item.hide === true || item.hide === 1; const is_disabled = item.enable === false || item.enable === 0; // Only block if explicitly false/0 - + return !is_hidden && !is_disabled; }); @@ -74,49 +73,49 @@ } } - function add_activity_log({ - action = 'idaa_meetings_page', - action_with = 'none' - }: { - action?: string; - action_with?: string; - }) { - let last_cache_refresh_iso = new Date($ae_loc?.last_cache_refresh); + // function add_activity_log({ + // action = 'idaa_meetings_page', + // action_with = 'none' + // }: { + // action?: string; + // action_with?: string; + // }) { + // let last_cache_refresh_iso = new Date($ae_loc?.last_cache_refresh); - let activity_description = ` - ${$idaa_loc.novi_full_name ?? 'none'} ${$idaa_loc.novi_email ?? 'no-email'} - allow=${$ae_loc?.allow_access} - last_cache_refresh=${last_cache_refresh_iso.toLocaleString()} - data_route=${data?.route?.toString() ?? 'unknown'} - `; + // let activity_description = ` + // ${$idaa_loc.novi_full_name ?? 'none'} ${$idaa_loc.novi_email ?? 'no-email'} + // allow=${$ae_loc?.allow_access} + // last_cache_refresh=${last_cache_refresh_iso.toLocaleString()} + // data_route=${data?.route?.toString() ?? 'unknown'} + // `; - let data_kv = { - external_client_id: data?.route.id, - name: `IDAA: ${$idaa_loc.novi_full_name ?? 'none'} ${$idaa_loc.novi_email ?? ''}`, - description: activity_description ?? null, - object_type: 'event', // archive, post, event - url_root: data?.url.origin, - url_full_path: data?.url.pathname, - url_params: data?.url.searchParams.toString(), - action: action, - action_with: action_with ?? 'none', - meta_json: { - allow_access: $ae_loc?.allow_access, - last_cache_refresh: $ae_loc?.last_cache_refresh, - last_cache_refresh_iso: last_cache_refresh_iso.toISOString(), - last_cache_refresh_locale: last_cache_refresh_iso.toLocaleString(), - access_level: $ae_loc?.access_level, - iframe: $ae_loc?.iframe - } - }; + // let data_kv = { + // external_client_id: data?.route.id, + // name: `IDAA: ${$idaa_loc.novi_full_name ?? 'none'} ${$idaa_loc.novi_email ?? ''}`, + // description: activity_description ?? null, + // object_type: 'event', // archive, post, event + // url_root: data?.url.origin, + // url_full_path: data?.url.pathname, + // url_params: data?.url.searchParams.toString(), + // action: action, + // action_with: action_with ?? 'none', + // meta_json: { + // allow_access: $ae_loc?.allow_access, + // last_cache_refresh: $ae_loc?.last_cache_refresh, + // last_cache_refresh_iso: last_cache_refresh_iso.toISOString(), + // last_cache_refresh_locale: last_cache_refresh_iso.toLocaleString(), + // access_level: $ae_loc?.access_level, + // iframe: $ae_loc?.iframe + // } + // }; - core_func.create_ae_obj__activity_log({ - api_cfg: $ae_api, - account_id: $ae_loc.account_id, - data_kv: data_kv, - log_lvl: log_lvl - }); - } + // core_func.create_ae_obj__activity_log({ + // api_cfg: $ae_api, + // account_id: $ae_loc.account_id, + // data_kv: data_kv, + // log_lvl: log_lvl + // }); + // }