491 lines
25 KiB
Markdown
491 lines
25 KiB
Markdown
# Stability Patterns for liveQuery + Svelte 5
|
|
|
|
Dexie's `liveQuery` works well with Svelte 5 runes, but the combination requires a few stable patterns so queries don't get recreated unintentionally and components render correctly on a "cold start" (empty IndexedDB).
|
|
- Keep the observable instance stable: wrap `liveQuery` in a stable `$derived` so the observable isn't recreated on every render. Recreate the `liveQuery` only when explicit dependencies change (IDs, filters, or search keys).
|
|
|
|
```typescript
|
|
// stable derived wrapper — only recreated when `id` changes
|
|
let lq__obj = $derived(
|
|
(() => {
|
|
// capture the dependency(s) in a single stable closure
|
|
const id = url_id;
|
|
return liveQuery(async () => {
|
|
if (!id) return null;
|
|
console.log('[LQ] running for id=', id);
|
|
return await db.table.get(id);
|
|
});
|
|
})()
|
|
);
|
|
```
|
|
|
|
- Use `$derived.by(() => ...)` where available in your runes shim to build queries from a computed set of inputs (IDs list, search params). This preserves a stable observable instance while still reacting to explicit dependency changes.
|
|
|
|
- Avoid capturing mutable objects or inline expressions in the `liveQuery` closure. If the closure captures a changing reference, the query may be recreated unexpectedly or miss the first write.
|
|
## Common Gotchas and Fixes (Why things sometimes need multiple refreshes)
|
|
|
|
- Cold start (IDB empty) + non-blocking API writes: If you mount a component before data is written to IDB, `liveQuery` may run against an empty DB. The API write will populate IDB later, but sometimes a chain of dependent queries (e.g., presentations -> presenters) won't all rerun in the order you expect. The symptoms you described — session shows after one refresh, presenters only after a second — are consistent with either (a) queries recreated in the wrong order or (b) dependent store values being set only after some subscriptions are already created.
|
|
|
|
### Critical Discovery (2026-02-26): The "try_cache: false" Bug
|
|
|
|
**Symptom:** Nested data (e.g., Session → Presentations → Presenters) requires multiple manual refreshes to display on cold-start, even when using blocking loads.
|
|
|
|
**Root Cause:** Two interconnected issues in nested data loaders:
|
|
1. **Disabled caching in nested loads**: Parent loads were passing `try_cache: false` to child loads, meaning presentations and presenters were fetched from API but **never written to IndexedDB**.
|
|
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery *before* IndexedDB writes completed, causing race conditions.
|
|
|
|
**Example of the Bug:**
|
|
```typescript
|
|
// Session loader (BROKEN)
|
|
await db_save_ae_obj_li__ae_obj({ table: 'session', obj_li: [session] });
|
|
// Loads presentations but disables caching ❌
|
|
return await load_presentations({ ..., try_cache: false });
|
|
// Presentations fetch from API ✅
|
|
// Presentations SKIP IndexedDB write ❌
|
|
// Presenters SKIP IndexedDB write ❌
|
|
// Component mounts, liveQuery finds only session ❌
|
|
```
|
|
|
|
**The Fix:**
|
|
```typescript
|
|
// Session loader (FIXED)
|
|
await db_save_ae_obj_li__ae_obj({ table: 'session', obj_li: [session] });
|
|
await Promise.resolve(); // Yield to observers
|
|
// Preserve parent's try_cache value ✅
|
|
return await load_presentations({ ..., try_cache });
|
|
// Presentations fetch AND write to IDB ✅
|
|
await Promise.resolve(); // Yield to observers
|
|
// Presenters fetch AND write to IDB ✅
|
|
await Promise.resolve(); // Yield to observers
|
|
// Component mounts, liveQuery finds all data ✅
|
|
```
|
|
|
|
**Key Lessons:**
|
|
1. **Always preserve `try_cache` through nested loads** unless you have a specific reason to disable caching for that operation
|
|
2. **Add `await Promise.resolve()` after IndexedDB writes** to ensure Dexie's liveQuery observers fire before the function returns
|
|
3. **Block on nested loads with `await Promise.all()`** instead of fire-and-forget `forEach()` when the page needs complete data for first render
|
|
|
|
Fixes:
|
|
- Prefer the "Blocking Loader" when you can: `await` the API call in `+page.ts` so IDB is populated before Svelte mounts.
|
|
- If you cannot block, return an `initial_*` object from `+page.ts` and use it as an immediate fallback in your component so the UI renders from that payload while `liveQuery` takes over for subsequent updates. Example from Aether:
|
|
|
|
```svelte
|
|
<Comp_event_presentation_obj_li
|
|
lq__event_presentation_obj_li={$lq__event_presentation_obj_li ?? data.initial_session_obj?.event_presentation_li ?? []}
|
|
{log_lvl}
|
|
/>
|
|
```
|
|
|
|
- Ensure store IDs are set before subscribers that depend on them are created. Use `untrack()` (or an equivalent non-reactive assignment) to set IDs in stores during initialization so components subscribe to the correct IDs immediately:
|
|
|
|
```typescript
|
|
$effect(() => {
|
|
if (!ae_acct) return;
|
|
untrack(() => {
|
|
$events_slct.event_id = url_event_id;
|
|
$events_slct.event_session_id = url_session_id;
|
|
});
|
|
});
|
|
```
|
|
|
|
- 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.
|
|
|
|
## 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 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.
|
|
|
|
- Sessions / Presentations: The session page demonstrates several best practices:
|
|
- Use `url_*` constants (derived from `data.params`) so the `liveQuery` closure captures a stable value instead of the reactive store directly.
|
|
- Provide `initial_session_obj` from `+page.ts` as a first-draw fallback to child components.
|
|
- Use `$derived.by(() => liveQuery(...))` for presentation lists so the observable instance is stable across renders and recreated only when `event_session_id` or `search` changes.
|
|
|
|
- Search pages with persisted filters or saved query text should keep the auto-search trigger in a page-level `$effect`, but the duplicate guard should live inside the actual search executor. That preserves the first page-load search while blocking repeated identical reruns from localStorage-backed rerenders. In practice:
|
|
- derive a single `qry_key` from the search inputs
|
|
- debounce in the `$effect`
|
|
- compare `qry_key` against a `last_executed_key` inside `handle_search_refresh()`
|
|
- keep transient loading flags and trigger counters in session state when the value is only used to force a refresh, not as a persisted preference
|
|
|
|
Example (presentation list pattern):
|
|
```typescript
|
|
let lq__event_presentation_obj_li = $derived(
|
|
liveQuery(async () => {
|
|
if (!url_session_id) return [];
|
|
console.log('[LQ] Querying Presentations for Session:', url_session_id);
|
|
return await db_events.presentation.where('event_session_id').equals(url_session_id).sortBy('name');
|
|
})
|
|
);
|
|
```
|
|
|
|
## Debugging Checklist
|
|
|
|
- Add a small `console.log` inside each `liveQuery` closure to confirm when it runs and what `id` it sees.
|
|
- Verify that `+page.ts` either `await`s critical loads or returns `initial_*` payloads for first-render hydration.
|
|
- Confirm that dependent store values (selected IDs) are assigned before components subscribe — use `untrack` to prevent extra reactive cycles.
|
|
- If a search page stops auto-loading after a localStorage change, check whether the duplicate guard was placed in the `$effect` instead of the executor. Guarding too early can suppress the initial search; guard at execution time instead.
|
|
- If a broad Dexie-backed list shows fewer rows than a narrower filter, look for a limit or revalidation step overwriting the local IDB result set. Broad views should stay unbounded unless the user is actually narrowing by text.
|
|
- Ensure your `liveQuery` closures return quickly and do not throw; any exception inside the query can stop updates.
|
|
- If a dependent query appears stale, temporarily add `await 0` in the upstream query or an explicit `Promise.resolve()` after the IDB write to force the microtask queue to flush during debugging.
|
|
|
|
## Summary Recommendations
|
|
|
|
- Prefer blocking loads for primary views when first-render correctness matters.
|
|
- Use `initial_*` fallback data when non-blocking loads are required.
|
|
- Wrap `liveQuery` in stable `$derived` instances and only recreate when explicit inputs change.
|
|
- Use `untrack` to set selection IDs during initialization to avoid subscribe-order bugs.
|
|
- Add targeted logs inside `liveQuery` closures to diagnose ordering and subscription behavior.
|
|
|
|
These patterns are deliberately conservative — they trade minimal blocking or small explicit fallbacks for predictable first-render behaviour. The Aether app's Journals and Event session pages are working examples of these techniques in practice.
|
|
|
|
## Examples in this repository
|
|
|
|
The following files demonstrate stable `liveQuery` usage, `initial_*` fallbacks, and stable `$derived` wrappers used across the Aether app. Inspect these for copy/paste patterns and concrete implementations.
|
|
|
|
- Journals page (stable LQ + search patterns): [src/routes/journals/[journal_id]/+page.svelte](src/routes/journals/[journal_id]/+page.svelte#L51)
|
|
- Journals layout (blocking background loader): [src/routes/journals/[journal_id]/+layout.ts](src/routes/journals/[journal_id]/+layout.ts#L1)
|
|
- Session page with URL capture + initial fallback: [src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte#L41)
|
|
- Presentation management overview (stable derived + search): [src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte#L70)
|
|
- Event settings example (simple observable): [src/routes/events/[event_id]/settings/+page.svelte](src/routes/events/[event_id]/settings/+page.svelte#L51)
|
|
- Badge/detail pages (examples of nested LQ): [src/routes/events/[event_id]/(badges)/badges/+page.svelte](src/routes/events/[event_id]/(badges)/badges/+page.svelte#L66)
|
|
|
|
Refer to these files when you need concrete code examples to adopt the patterns described above.
|
|
|
|
## References
|
|
|
|
This document provides a guide to integrating Svelte (with a focus on Runes) and Dexie.js for building reactive web applications. It covers key concepts and best practices for managing reactivity between Svelte components and the Dexie.js database.
|
|
|
|
## Svelte 5 Migration Guide
|
|
|
|
Svelte 5 introduces "runes" as a new way to manage reactivity. This is a major change from previous versions of Svelte, and it's important to understand the breaking changes before migrating.
|
|
|
|
### Key Breaking Changes
|
|
|
|
- **`let` is no longer reactive:** In Svelte 4, any `let` variable declared in the top-level scope of a component was automatically reactive. In Svelte 5, you must explicitly declare reactive state using the `$state` rune.
|
|
- **`$:` is replaced by `$derived` and `$effect`:** The `$` label is no longer used for reactive statements. Instead, you should use the `$derived` rune for computed values and the `$effect` rune for side effects.
|
|
- **`export let` is replaced by `$props`:** Component props are now declared using the `$props` rune, which provides a more flexible and explicit way to define component APIs.
|
|
- **Event handling:** The `on:` directive is replaced by event attributes (e.g., `onclick`). Component events are now handled using callback props instead of `createEventDispatcher`.
|
|
- **Slots are replaced by snippets:** The `<slot>` element is replaced by the `{#snippet ...}` block, which provides a more powerful and flexible way to pass content to components.
|
|
|
|
For a complete list of breaking changes, refer to the [Svelte 5 migration guide](https://svelte.dev/docs/svelte/v5-migration-guide).
|
|
|
|
## Dexie.js Quick Reference
|
|
|
|
Dexie.js is a lightweight, minimalistic wrapper for IndexedDB that makes it easier to work with client-side databases.
|
|
|
|
### Key Classes and Methods
|
|
|
|
- **`Dexie`:** The main class for creating and managing IndexedDB databases.
|
|
- `new Dexie(databaseName)`: Creates a new database instance.
|
|
- `version(versionNumber).stores({ ... })`: Defines the database schema.
|
|
- **`Table`:** Represents an object store (table) in the database.
|
|
- `add(item)`: Adds a new item to the table.
|
|
- `put(item)`: Adds or updates an item in the table.
|
|
- `update(key, changes)`: Updates an existing item.
|
|
- `delete(key)`: Deletes an item by its primary key.
|
|
- `get(key)`: Retrieves an item by its primary key.
|
|
- `where(index)`: Starts a query using an index.
|
|
- `toArray()`: Retrieves all items from the table as an array.
|
|
- **`Collection`:** Represents a collection of items resulting from a query.
|
|
- `toArray()`: Retrieves all items in the collection as an array.
|
|
- `first()`: Retrieves the first item in the collection.
|
|
- `last()`: Retrieves the last item in the collection.
|
|
- `each(callback)`: Iterates over each item in the collection.
|
|
- `modify(changes)`: Updates all items in the collection.
|
|
- `delete()`: Deletes all items in the collection.
|
|
|
|
For a complete list of API methods, refer to the [Dexie.js API Reference](https://dexie.org/docs/API-Reference).
|
|
|
|
## Integrating Svelte Runes and Dexie.js
|
|
|
|
The combination of Svelte Runes and Dexie.js allows for the creation of highly reactive and efficient web applications.
|
|
|
|
### The `liveQuery` Function
|
|
|
|
Dexie.js provides a `liveQuery` function that returns an observable of the query result. This observable can be used to automatically update the UI whenever the data in the database changes.
|
|
|
|
### Using `liveQuery` with Svelte Runes
|
|
|
|
To use `liveQuery` with Svelte Runes, you can create a custom readable store that wraps the `liveQuery` observable. This store can then be used in your Svelte components to display and interact with the data.
|
|
|
|
**1. Create a `liveQuery` store:**
|
|
|
|
```typescript
|
|
import { liveQuery } from 'dexie';
|
|
import { readable } from 'svelte/store';
|
|
import { db } from './db'; // Your Dexie database instance
|
|
|
|
export function createLiveQueryStore<T>(query: () => T | Promise<T>) {
|
|
return readable<T | undefined>(undefined, (set) => {
|
|
const subscription = liveQuery(query).subscribe({
|
|
next: (result) => set(result),
|
|
error: (error) => console.error(error)
|
|
});
|
|
return () => subscription.unsubscribe();
|
|
});
|
|
}
|
|
```
|
|
|
|
**2. Use the `createLiveQueryStore` in your component:**
|
|
|
|
```html
|
|
<script>
|
|
import { createLiveQueryStore } from './stores';
|
|
import { db } from './db';
|
|
|
|
const friends = createLiveQueryStore(() => db.friends.toArray());
|
|
</script>
|
|
|
|
<ul>
|
|
{#if $friends} {#each $friends as friend}
|
|
<li>{friend.name}</li>
|
|
{/each} {/if}
|
|
</ul>
|
|
```
|
|
|
|
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.
|
|
|
|
## SvelteKit Layout Hierarchy: Security and Execution Order
|
|
|
|
Understanding *when* SvelteKit code runs is critical for private-data modules like IDAA.
|
|
|
|
### Execution order on any navigation
|
|
|
|
```text
|
|
1. +layout.ts / +page.ts ← run FIRST — before any component mounts
|
|
also fired by SvelteKit link prefetch (on hover)
|
|
2. Parent +layout.svelte mounts → its $effect blocks run
|
|
3. Child +layout.svelte mounts → only if parent called {#render children?.()}
|
|
4. +page.svelte mounts → only if every parent in the chain rendered children
|
|
5. $effect blocks in all of the above run after mount
|
|
```
|
|
|
|
### The auth-gate consequence
|
|
|
|
A `{:else if authenticated} {@render children?.()}` block in a `+layout.svelte`
|
|
controls whether **everything below it** ever mounts. If the gate blocks rendering,
|
|
no child layout or page component instantiates — their `$effect` blocks, event
|
|
handlers, and liveQuery closures never run.
|
|
|
|
```svelte
|
|
<!-- (idaa)/+layout.svelte -->
|
|
{:else if $ae_loc.trusted_access || $idaa_loc.novi_verified}
|
|
{@render children?.()} ← children only mount if this branch runs
|
|
{:else}
|
|
<p>Access Denied</p> ← children never mount; their $effects never run
|
|
{/if}
|
|
```
|
|
|
|
**`$effect` blocks inside a child component cannot bypass a parent layout auth gate.**
|
|
They are already inside the gate. Adding redundant auth guards to `$effect` blocks
|
|
that only run after a parent has already verified access is unnecessary — and misleads
|
|
future readers into thinking the parent gate alone is not sufficient.
|
|
|
|
### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts`
|
|
|
|
Universal load functions run *before* components mount and *before* layout effects
|
|
execute. They also fire during SvelteKit link prefetch — triggered by the user
|
|
hovering a link, even if they never navigate. This makes them unsafe for private data:
|
|
|
|
```text
|
|
User hovers an /idaa/ link →
|
|
SvelteKit prefetch fires →
|
|
+page.ts runs (no layout has mounted yet, no auth gate has run) →
|
|
API call / IDB write happens for an unauthenticated user
|
|
```
|
|
|
|
**Rule for private modules (IDAA, Journals):** `+page.ts` and `+layout.ts` files must
|
|
not call any data load functions that write to IDB. Move all data loading to `$effect`
|
|
blocks in the corresponding `+page.svelte`, gated inside the auth-checked layout render.
|
|
The comments in every `+page.ts` under `src/routes/idaa/(idaa)/` explain this pattern.
|
|
|
|
### The `$effect` auth guards in IDAA `+page.svelte` files
|
|
|
|
These ARE still useful — but for a different reason than layout bypass:
|
|
|
|
```ts
|
|
// In bb/+page.svelte
|
|
$effect(() => {
|
|
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
|
posts_func.load_ae_obj_li__post(...)
|
|
});
|
|
```
|
|
|
|
Because `$ae_loc` is a Svelte 4 coarse-grained store, any unrelated write to it
|
|
(iframe height, SWR reload) re-triggers this `$effect`. The guard prevents a spurious
|
|
API call if `$idaa_loc.novi_verified` has been cleared between re-runs (e.g. TTL
|
|
expiry mid-session). It is a reactivity guard, not a layout-bypass guard.
|
|
|
|
---
|
|
|
|
## Page Load Strategies (Avoiding the "Waterfall")
|
|
|
|
When loading data for a primary page view (e.g., viewing a specific Journal, Session, or Person), you must choose a synchronization strategy to ensure the UI renders correctly on the first load.
|
|
|
|
### ❌ The "Fire & Forget" Anti-Pattern (AVOID)
|
|
Triggering a background load in `+page.ts` without `await` leads to race conditions.
|
|
1. `+page.svelte` mounts immediately.
|
|
2. `liveQuery` runs against an empty IndexedDB.
|
|
3. API data arrives later and writes to IndexedDB.
|
|
4. **Failure:** Svelte 5 + Dexie `liveQuery` may not automatically detect this first "cold start" update without a manual refresh.
|
|
|
|
### ✅ The "Blocking Loader" Pattern (RECOMMENDED)
|
|
Ensure the data is in IndexedDB **before** the component mounts.
|
|
1. In `+page.ts`, `await` the API load function.
|
|
2. In `+page.svelte`, the `liveQuery` will see the data immediately upon mount.
|
|
|
|
**Example (+page.ts):**
|
|
```typescript
|
|
export async function load({ params }) {
|
|
// Blocking await ensures IDB is populated
|
|
await journals_func.load_ae_obj_id__journal({
|
|
journal_id: params.journal_id,
|
|
try_cache: true
|
|
});
|
|
return {};
|
|
}
|
|
```
|
|
|
|
### ✅ The "Hydrate & Subscribe" Pattern (ADVANCED)
|
|
If you must use non-blocking loads, you must pass the initial data to the component to "hydrate" the state before the subscription takes over.
|
|
|
|
1. In `+page.ts`, `await` the load and **return the object**.
|
|
2. In `+page.svelte`, use the returned object as a fallback or initial state.
|
|
|
|
**Example (+page.svelte):**
|
|
```svelte
|
|
<script>
|
|
let { data } = $props();
|
|
let lq__obj = $derived(liveQuery(async () => db.table.get(id)));
|
|
</script>
|
|
|
|
<!-- Use fallback to handle the gap before liveQuery emits -->
|
|
{#if $lq__obj || data.initial_obj}
|
|
<View object={$lq__obj ?? data.initial_obj} />
|
|
{/if}
|
|
```
|
|
|
|
## The `untrack()` Reactive-Tracking Trap
|
|
|
|
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you *need* to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
|
|
|
|
### Symptom
|
|
|
|
An effect runs once, reads a store value inside `untrack()`, takes an early-exit path (e.g. "no API key → skip"), and never retries — even after the store value is updated by a background process.
|
|
|
|
### Real Example (IDAA Novi Verification Bug — 2026-03-25)
|
|
|
|
The IDAA layout verifies Novi UUIDs. `site_cfg_json` (which contains the Novi API key) was read **inside** `untrack()`:
|
|
|
|
```typescript
|
|
// BUG: site_cfg_json read inside untrack → one-shot, never retries
|
|
$effect(() => {
|
|
if (!browser) return;
|
|
const uuid = data.url.searchParams.get('uuid'); // tracked ✓
|
|
|
|
untrack(() => {
|
|
const site_cfg_json = $ae_loc.site_cfg_json; // ← NOT tracked ✗
|
|
const api_key = site_cfg_json?.novi_idaa_api_key ?? null;
|
|
if (!api_key) return; // exits silently on first load with stale cache
|
|
verify_novi_uuid(uuid, api_key, ...);
|
|
});
|
|
});
|
|
```
|
|
|
|
On first load, the Dexie cache returned a stale `site_cfg_json` missing the API key. The effect exited early. The background refresh later updated `$ae_loc.site_cfg_json`, but because `site_cfg_json` was consumed inside `untrack()`, the effect never re-ran.
|
|
|
|
**Fix:** Move the dependency read **outside** `untrack()`:
|
|
|
|
```typescript
|
|
// FIX: site_cfg_json tracked outside untrack → effect re-runs when it changes
|
|
$effect(() => {
|
|
if (!browser) return;
|
|
const uuid = data.url.searchParams.get('uuid'); // tracked ✓
|
|
const site_cfg_json = $ae_loc.site_cfg_json; // tracked ✓ — effect re-runs on change
|
|
|
|
untrack(() => {
|
|
// Guard: already verified for this UUID — don't repeat the round-trip
|
|
if ($idaa_loc.novi_verified && $idaa_loc.novi_uuid === uuid) return;
|
|
|
|
const api_key = site_cfg_json?.novi_idaa_api_key ?? null;
|
|
if (!api_key) return;
|
|
verify_novi_uuid(uuid, api_key, ...);
|
|
});
|
|
});
|
|
```
|
|
|
|
The guard inside `untrack()` is important: without it, every unrelated change to `$ae_loc` would re-trigger verification.
|
|
|
|
### Rule of Thumb
|
|
|
|
Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re-run if this value changes?"**
|
|
|
|
- If yes → read it **outside** `untrack()`, and add a guard inside to prevent redundant work.
|
|
- If no → `untrack()` is correct.
|
|
|
|
---
|
|
|
|
## Svelte 5 Binding Pitfalls
|
|
|
|
### 1. `props_invalid_value` (The "Expression Binding" Error)
|
|
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
|
|
|
|
**❌ Invalid Pattern (Causes Compile Error):**
|
|
Attempting to normalize a value *inside* the binding will fail.
|
|
```svelte
|
|
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
|
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
|
```
|
|
|
|
**✅ Correct Pattern:**
|
|
Ensure the source value is already normalized before binding, or use a reactive effect to handle the fallback.
|
|
```typescript
|
|
// Normalize in an effect or derivation
|
|
$effect(() => {
|
|
if ($events_slct.event_session_id === undefined) {
|
|
$events_slct.event_session_id = null;
|
|
}
|
|
});
|
|
```
|
|
```svelte
|
|
<!-- Bind directly to the normalized property -->
|
|
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id} />
|
|
```
|
|
|
|
---
|
|
|
|
## 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();
|
|
```
|
|
|
|
## References
|
|
* https://dexie.org/llms.txt - Dexie.js and Dexie Cloud — LLM Guide and Documentation Summary |