max / balanced_breakfast
5 files changed,
+79 insertions,
-79 deletions
| @@ -49,17 +49,17 @@ Shared libraries from `../../MNW/shared/`: [theme-common](../../MNW/shared/theme | |||
| 49 | 49 | ||
| 50 | 50 | ## Features | |
| 51 | 51 | ||
| 52 | - | - **Unified timeline** -- RSS, Atom, JSON Feed, Hacker News, arXiv, and custom sources merged into one feed | |
| 53 | - | - **Plugin system** -- Rhai scripting for extensible feed fetching (write a plugin for any source) | |
| 54 | - | - **Reader view** -- clean article rendering with HTML sanitization via DocEngine | |
| 55 | - | - **Search** -- FTS5 full-text search across all items with sanitized indexing | |
| 56 | - | - **Organization** -- tags, starred items, read/unread tracking, query feeds (saved dynamic filters) | |
| 57 | - | - **Auto-fetch scheduling** -- configurable per-plugin fetch intervals, circuit breaker after consecutive failures | |
| 58 | - | - **Feed health tracking** -- visual status indicators (green/yellow/red) per source | |
| 59 | - | - **Cloud sync** -- SyncKit integration with E2E encryption (feeds, tags, read state, preferences) | |
| 60 | - | - **OTA updates** -- background update checker with consent dialog (Tauri updater v2) | |
| 61 | - | - **17 bundled themes** -- dark, light, and high-contrast variants in TOML format, system auto-detection | |
| 62 | - | - **Platforms** -- macOS, Windows, Linux (native via Tauri 2) | |
| 52 | + | - **Unified timeline**: RSS, Atom, JSON Feed, Hacker News, arXiv, and custom sources merged into one feed | |
| 53 | + | - **Plugin system**: Rhai scripting for extensible feed fetching (write a plugin for any source) | |
| 54 | + | - **Reader view**: clean article rendering with HTML sanitization via DocEngine | |
| 55 | + | - **Search**: FTS5 full-text search across all items with sanitized indexing | |
| 56 | + | - **Organization**: tags, starred items, read/unread tracking, query feeds (saved dynamic filters) | |
| 57 | + | - **Auto-fetch scheduling**: configurable per-plugin fetch intervals, circuit breaker after consecutive failures | |
| 58 | + | - **Feed health tracking**: visual status indicators (green/yellow/red) per source | |
| 59 | + | - **Cloud sync**: SyncKit integration with E2E encryption (feeds, tags, read state, preferences) | |
| 60 | + | - **OTA updates**: background update checker with consent dialog (Tauri updater v2) | |
| 61 | + | - **17 bundled themes**: dark, light, and high-contrast variants in TOML format, system auto-detection | |
| 62 | + | - **Platforms**: macOS, Windows, Linux (native via Tauri 2) | |
| 63 | 63 | ||
| 64 | 64 | ## Plugin Authoring | |
| 65 | 65 |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | # Balanced Breakfast -- Architecture | |
| 1 | + | # Balanced Breakfast: Architecture | |
| 2 | 2 | ||
| 3 | 3 | When it comes to media and news, it's good to be a picky eater. | |
| 4 | 4 | ||
| @@ -26,11 +26,11 @@ Dependency flow: `bb-interface` (leaf) --> `bb-core`, `bb-feed`, `bb-db` --> `sr | |||
| 26 | 26 | ||
| 27 | 27 | The `Orchestrator` (`bb-core::orchestrator`) is the central coordination point. It owns the `Database` and a `PluginManager` behind an `Arc<RwLock<>>`. Its responsibilities: | |
| 28 | 28 | ||
| 29 | - | - **Plugin lifecycle** -- load `.rhai` scripts from the plugins directory, initialize them with config from the DB, and provide fetch/shutdown operations. | |
| 30 | - | - **Fetch execution** -- call a plugin's `fetch()`, strip tracking parameters from item URLs and HTML bodies, upsert results into the DB via the items repository, and record success/failure on the feed. | |
| 31 | - | - **Circuit breaker** -- after 10 consecutive fetch failures (`CIRCUIT_BREAKER_THRESHOLD`), the feed is marked `circuit_broken` and excluded from auto-fetch until manually reset. | |
| 32 | - | - **Secret management** -- holds an optional AES-256-GCM key. On startup, encrypts any plaintext Secret fields in existing feed configs (migration from legacy plaintext). | |
| 33 | - | - **Fetch-all** -- iterates all loaded plugins and fetches each, collecting total item counts. | |
| 29 | + | - **Plugin lifecycle**: load `.rhai` scripts from the plugins directory, initialize them with config from the DB, and provide fetch/shutdown operations. | |
| 30 | + | - **Fetch execution**: call a plugin's `fetch()`, strip tracking parameters from item URLs and HTML bodies, upsert results into the DB via the items repository, and record success/failure on the feed. | |
| 31 | + | - **Circuit breaker**: after 10 consecutive fetch failures (`CIRCUIT_BREAKER_THRESHOLD`), the feed is marked `circuit_broken` and excluded from auto-fetch until manually reset. | |
| 32 | + | - **Secret management**: holds an optional AES-256-GCM key. On startup, encrypts any plaintext Secret fields in existing feed configs (migration from legacy plaintext). | |
| 33 | + | - **Fetch-all**: iterates all loaded plugins and fetches each, collecting total item counts. | |
| 34 | 34 | ||
| 35 | 35 | The orchestrator does not own the fetch scheduler or background tasks. Those are managed by `AppState` in the Tauri layer. | |
| 36 | 36 | ||
| @@ -42,17 +42,17 @@ Plugins are `.rhai` text files dropped into the plugins directory. The Rhai engi | |||
| 42 | 42 | ||
| 43 | 43 | Every plugin must define four functions: | |
| 44 | 44 | ||
| 45 | - | - `id()` -- returns a unique string identifier (e.g. `"rss"`, `"hackernews"`) | |
| 46 | - | - `name()` -- returns a human-readable display name | |
| 47 | - | - `config_schema()` -- returns a map describing configuration fields (key, label, field_type, required, default, options) | |
| 48 | - | - `fetch(config, cursor)` -- returns `{ items: [...], has_more: bool, next_cursor: string? }` | |
| 45 | + | - `id()`: returns a unique string identifier (e.g. `"rss"`, `"hackernews"`) | |
| 46 | + | - `name()`: returns a human-readable display name | |
| 47 | + | - `config_schema()`: returns a map describing configuration fields (key, label, field_type, required, default, options) | |
| 48 | + | - `fetch(config, cursor)`: returns `{ items: [...], has_more: bool, next_cursor: string? }` | |
| 49 | 49 | ||
| 50 | 50 | An optional `capabilities()` function can declare pagination support, custom fetch intervals, auth requirements, etc. | |
| 51 | 51 | ||
| 52 | 52 | ### Sandboxing | |
| 53 | 53 | ||
| 54 | - | - **Operations cap:** `max_operations(100_000)` -- a typical RSS fetch costs 1k-5k ops; this catches infinite loops. | |
| 55 | - | - **Expression depth:** `max_expr_depths(128, 128)` -- prevents stack overflows from deeply nested or recursive scripts. | |
| 54 | + | - **Operations cap:** `max_operations(100_000)`. A typical RSS fetch costs 1k-5k ops; this catches infinite loops. | |
| 55 | + | - **Expression depth:** `max_expr_depths(128, 128)`. Prevents stack overflows from deeply nested or recursive scripts. | |
| 56 | 56 | - **HTTP timeout:** 15 seconds per request. | |
| 57 | 57 | - **Response size:** 2 MB cap per response body. | |
| 58 | 58 | - **Request limit:** 100 HTTP requests per `fetch()` invocation. Counter resets at the start of each fetch. | |
| @@ -96,10 +96,10 @@ The `FeedGenerator` (`bb-feed::generator`) reads items from the database, applie | |||
| 96 | 96 | - Item-level tags, feed-level tags, and query feed conditions (title/author/body contains, equals, not_contains, matches_regex) run in-memory after the SQL query. | |
| 97 | 97 | ||
| 98 | 98 | **Ordering** is applied in-memory after filtering: | |
| 99 | - | - `Chronological` -- newest first (default) | |
| 100 | - | - `Score` -- highest score first, with chronological tiebreak | |
| 101 | - | - `UnreadFirst` -- unread items before read, chronological within each group | |
| 102 | - | - `StarredFirst` -- starred items before unstarred, chronological within each group | |
| 99 | + | - `Chronological`: newest first (default) | |
| 100 | + | - `Score`: highest score first, with chronological tiebreak | |
| 101 | + | - `UnreadFirst`: unread items before read, chronological within each group | |
| 102 | + | - `StarredFirst`: starred items before unstarred, chronological within each group | |
| 103 | 103 | ||
| 104 | 104 | **Pagination** fetches `page_size + 1` items to detect whether more pages exist, then truncates to the exact page size. | |
| 105 | 105 | ||
| @@ -123,12 +123,12 @@ SQLite via sqlx with compile-time migrations (12 migrations). The `Database` str | |||
| 123 | 123 | ||
| 124 | 124 | ### Repositories | |
| 125 | 125 | ||
| 126 | - | - `FeedsRepository` -- CRUD, enable/disable, last_fetch updates, fetch failure recording, circuit breaker management | |
| 127 | - | - `ItemsRepository` -- upsert (dedup by external_id), read/star toggling, paginated listing (by busser, by feed, unread, starred), FTS5 search, counts, stale item deletion | |
| 128 | - | - `TagsRepository` -- per-feed tag assignment, distinct tag listing, bulk feed-tag pairs | |
| 129 | - | - `StateRepository` -- busser key-value state (get/set/delete by busser_id + key) | |
| 130 | - | - `ConfigRepository` -- user_config key-value pairs (get/set/delete) | |
| 131 | - | - `QueryFeedsRepository` -- query feed CRUD (create/update/delete/list) | |
| 126 | + | - `FeedsRepository`: CRUD, enable/disable, last_fetch updates, fetch failure recording, circuit breaker management | |
| 127 | + | - `ItemsRepository`: upsert (dedup by external_id), read/star toggling, paginated listing (by busser, by feed, unread, starred), FTS5 search, counts, stale item deletion | |
| 128 | + | - `TagsRepository`: per-feed tag assignment, distinct tag listing, bulk feed-tag pairs | |
| 129 | + | - `StateRepository`: busser key-value state (get/set/delete by busser_id + key) | |
| 130 | + | - `ConfigRepository`: user_config key-value pairs (get/set/delete) | |
| 131 | + | - `QueryFeedsRepository`: query feed CRUD (create/update/delete/list) | |
| 132 | 132 | ||
| 133 | 133 | FTS5 queries are sanitized by wrapping each search term in double quotes to prevent syntax injection (`AND`, `OR`, `NOT`, `NEAR` operators). The `^` prefix and `*` suffix characters are stripped. | |
| 134 | 134 | ||
| @@ -141,7 +141,7 @@ Balanced Breakfast integrates with the SyncKit client SDK for cross-device sync. | |||
| 141 | 141 | - Feed tags | |
| 142 | 142 | - User config (preferences) | |
| 143 | 143 | - Query feeds (saved filter rules) | |
| 144 | - | - Feed item user state (is_read, is_starred changes only -- not item content) | |
| 144 | + | - Feed item user state (is_read, is_starred changes only, not item content) | |
| 145 | 145 | ||
| 146 | 146 | **How it works:** SQLite triggers on synced tables write changes to `sync_changelog`. The sync engine pushes unpushed entries in batches of 500, pulls remote changes using a cursor, and applies them in FK-safe order (parents before children for upserts, children before parents for deletes). A `applying_remote` flag in `sync_state` suppresses trigger firing during remote change application to prevent echo loops. | |
| 147 | 147 | ||
| @@ -159,11 +159,11 @@ The sync scheduler runs on a configurable interval (default 15 minutes). Encrypt | |||
| 159 | 159 | ## Concurrency Model | |
| 160 | 160 | ||
| 161 | 161 | - **Tokio async runtime** (multi-threaded) drives all I/O: database queries, HTTP fetches, sync operations. | |
| 162 | - | - **`Arc<RwLock<PluginManager>>`** -- the orchestrator holds the plugin manager behind a Tokio RwLock. Read lock for fetches and schema queries; write lock only during plugin loading. | |
| 163 | - | - **`Arc<AppState>`** -- shared across Tauri commands and background tasks. Managed by Tauri's state system. | |
| 164 | - | - **AbortHandles** -- background tasks (auto-fetch loop, stale cleanup) store their `AbortHandle` in `AppState` behind `std::sync::Mutex`. On shutdown or task replacement, existing handles are aborted. | |
| 165 | - | - **Auto-fetch loop** -- checks every 60 seconds which plugins are due for a fetch based on their last_fetch timestamp and configured interval. | |
| 166 | - | - **Stale cleanup** -- runs every 6 hours, deleting read (non-starred) items older than 30 days. | |
| 162 | + | - **`Arc<RwLock<PluginManager>>`**: the orchestrator holds the plugin manager behind a Tokio RwLock. Read lock for fetches and schema queries; write lock only during plugin loading. | |
| 163 | + | - **`Arc<AppState>`**: shared across Tauri commands and background tasks. Managed by Tauri's state system. | |
| 164 | + | - **AbortHandles**: background tasks (auto-fetch loop, stale cleanup) store their `AbortHandle` in `AppState` behind `std::sync::Mutex`. On shutdown or task replacement, existing handles are aborted. | |
| 165 | + | - **Auto-fetch loop**: checks every 60 seconds which plugins are due for a fetch based on their last_fetch timestamp and configured interval. | |
| 166 | + | - **Stale cleanup**: runs every 6 hours, deleting read (non-starred) items older than 30 days. | |
| 167 | 167 | ||
| 168 | 168 | ## Frontend Architecture | |
| 169 | 169 |
| @@ -74,7 +74,7 @@ FTS5 virtual table in external content mode. Indexes `title`, `body`, and `bite_ | |||
| 74 | 74 | ||
| 75 | 75 | - External content source: `feed_items` (via `content=` and `content_rowid=`) | |
| 76 | 76 | - Kept in sync by three triggers: `feed_items_fts_insert`, `feed_items_fts_update`, `feed_items_fts_delete` | |
| 77 | - | - No data duplication -- FTS index references `feed_items` via rowid | |
| 77 | + | - No data duplication, FTS index references `feed_items` via rowid | |
| 78 | 78 | ||
| 79 | 79 | --- | |
| 80 | 80 | ||
| @@ -163,7 +163,7 @@ Index: `pushed`. | |||
| 163 | 163 | ## Design Patterns | |
| 164 | 164 | ||
| 165 | 165 | - **Two-tier display model:** `feed_items` has compact "bite_*" fields for list rendering and full `title`/`body` fields for detail view | |
| 166 | - | - **External content FTS:** `feed_items_fts` uses FTS5 external content mode -- zero data duplication, trigger-maintained | |
| 166 | + | - **External content FTS:** `feed_items_fts` uses FTS5 external content mode, zero data duplication, trigger-maintained | |
| 167 | 167 | - **Circuit breaker:** `feeds.circuit_broken` auto-disables feeds after `consecutive_failures` exceeds threshold | |
| 168 | 168 | - **Sync guard triggers:** All sync triggers check `applying_remote != '1'` to prevent re-recording server-pushed changes | |
| 169 | 169 | - **Synced tables:** `feeds`, `feed_tags`, `feed_items` (user state only: `is_read`/`is_starred`), `user_config`, `query_feeds` |
| @@ -102,19 +102,19 @@ const unsub = BB.state.subscribe('items', (newItems, oldItems) => { | |||
| 102 | 102 | ||
| 103 | 103 | | Property | Type | Domain | | |
| 104 | 104 | |----------|------|--------| | |
| 105 | - | | `sources` | Array | Data -- feed sources with counts | | |
| 106 | - | | `items` | Array | Data -- feed items for current view | | |
| 107 | - | | `queryFeeds` | Array | Data -- saved filter rules | | |
| 108 | - | | `allTags` | Array | Data -- distinct tags across all feeds | | |
| 109 | - | | `currentSource` | string | Filter -- busser_id or `''` for all, `'__saved__'` for saved | | |
| 110 | - | | `currentOrder` | string | Filter -- `'chronological'`, `'score'`, `'unread'`, `'starred'` | | |
| 111 | - | | `currentSearch` | string | Filter -- search query text | | |
| 112 | - | | `currentTag` | string | Filter -- tag filter or `''` for all | | |
| 113 | - | | `currentQueryFeed` | string/null | Filter -- active query feed ID | | |
| 114 | - | | `currentPage` | number | Pagination -- zero-indexed page | | |
| 115 | - | | `hasMore` | boolean | Pagination -- backend has more pages | | |
| 116 | - | | `selectedItemId` | string/null | Selection -- active item in list | | |
| 117 | - | | `selectedPluginId` | string/null | UI -- plugin chosen in add-feed flow | | |
| 105 | + | | `sources` | Array | Data: feed sources with counts | | |
| 106 | + | | `items` | Array | Data: feed items for current view | | |
| 107 | + | | `queryFeeds` | Array | Data: saved filter rules | | |
| 108 | + | | `allTags` | Array | Data: distinct tags across all feeds | | |
| 109 | + | | `currentSource` | string | Filter: busser_id or `''` for all, `'__saved__'` for saved | | |
| 110 | + | | `currentOrder` | string | Filter: `'chronological'`, `'score'`, `'unread'`, `'starred'` | | |
| 111 | + | | `currentSearch` | string | Filter: search query text | | |
| 112 | + | | `currentTag` | string | Filter: tag filter or `''` for all | | |
| 113 | + | | `currentQueryFeed` | string/null | Filter: active query feed ID | | |
| 114 | + | | `currentPage` | number | Pagination: zero-indexed page | | |
| 115 | + | | `hasMore` | boolean | Pagination: backend has more pages | | |
| 116 | + | | `selectedItemId` | string/null | Selection: active item in list | | |
| 117 | + | | `selectedPluginId` | string/null | UI: plugin chosen in add-feed flow | | |
| 118 | 118 | ||
| 119 | 119 | `resetPagination(clearSelection?)` is a convenience method that sets `currentPage` to 0 and optionally clears `selectedItemId`. | |
| 120 | 120 | ||
| @@ -147,9 +147,9 @@ User clicks item in list | |||
| 147 | 147 | ### Reactive subscriptions | |
| 148 | 148 | ||
| 149 | 149 | Two state subscriptions drive automatic re-rendering: | |
| 150 | - | - `BB.state.subscribe('items', render)` in items.js -- re-renders item list on any items change | |
| 151 | - | - `BB.state.subscribe('sources', render)` in sources.js -- re-renders sidebar on source list change | |
| 152 | - | - `BB.state.subscribe('items', onItemsChanged)` in detail.js -- merges updated summary fields into the detail view | |
| 150 | + | - `BB.state.subscribe('items', render)` in items.js: re-renders item list on any items change | |
| 151 | + | - `BB.state.subscribe('sources', render)` in sources.js: re-renders sidebar on source list change | |
| 152 | + | - `BB.state.subscribe('items', onItemsChanged)` in detail.js: merges updated summary fields into the detail view | |
| 153 | 153 | ||
| 154 | 154 | ## API Layer | |
| 155 | 155 | ||
| @@ -203,20 +203,20 @@ The two-step add-feed flow in feeds.js uses this system: step 1 shows a plugin p | |||
| 203 | 203 | ||
| 204 | 204 | Scripts load in order via `<script>` tags in `index.html`: | |
| 205 | 205 | ||
| 206 | - | 1. `bb.js` -- creates `window.BB` namespace with empty sub-objects | |
| 207 | - | 2. `themes.js` -- populates `BB.themes` (init, load, buildSelector) | |
| 208 | - | 3. `utils.js` -- populates `BB.utils` (escapeHtml, escapeAttr, sanitizeHtml, debounce, getErrorMessage) | |
| 209 | - | 4. `state.js` -- replaces `BB.state` with Proxy-wrapped reactive store | |
| 210 | - | 5. `api.js` -- replaces `BB.api` with invoke wrappers | |
| 211 | - | 6. `components.js` -- replaces `BB.ui` with toast/modal/form/confirm functions | |
| 212 | - | 7. `query-feeds.js` -- populates `BB.queryFeeds` (depends on utils, state, api, ui) | |
| 213 | - | 8. `sources.js` -- populates `BB.sources` (depends on utils, state, api, ui, queryFeeds) | |
| 214 | - | 9. `items.js` -- populates `BB.items` (depends on utils, state, api, ui, detail) | |
| 215 | - | 10. `detail.js` -- populates `BB.detail` (depends on utils, state, api, ui) | |
| 216 | - | 11. `feeds.js` -- populates `BB.feeds` (depends on utils, state, api, ui, sources, items) | |
| 217 | - | 12. `settings-sync.js` -- populates `BB.sync` (depends on api, ui) | |
| 218 | - | 13. `updater.js` -- populates `BB.updater` (depends on ui, utils) | |
| 219 | - | 14. `app.js` -- populates `BB.app`, auto-calls `init()` on DOMContentLoaded | |
| 206 | + | 1. `bb.js`: creates `window.BB` namespace with empty sub-objects | |
| 207 | + | 2. `themes.js`: populates `BB.themes` (init, load, buildSelector) | |
| 208 | + | 3. `utils.js`: populates `BB.utils` (escapeHtml, escapeAttr, sanitizeHtml, debounce, getErrorMessage) | |
| 209 | + | 4. `state.js`: replaces `BB.state` with Proxy-wrapped reactive store | |
| 210 | + | 5. `api.js`: replaces `BB.api` with invoke wrappers | |
| 211 | + | 6. `components.js`: replaces `BB.ui` with toast/modal/form/confirm functions | |
| 212 | + | 7. `query-feeds.js`: populates `BB.queryFeeds` (depends on utils, state, api, ui) | |
| 213 | + | 8. `sources.js`: populates `BB.sources` (depends on utils, state, api, ui, queryFeeds) | |
| 214 | + | 9. `items.js`: populates `BB.items` (depends on utils, state, api, ui, detail) | |
| 215 | + | 10. `detail.js`: populates `BB.detail` (depends on utils, state, api, ui) | |
| 216 | + | 11. `feeds.js`: populates `BB.feeds` (depends on utils, state, api, ui, sources, items) | |
| 217 | + | 12. `settings-sync.js`: populates `BB.sync` (depends on api, ui) | |
| 218 | + | 13. `updater.js`: populates `BB.updater` (depends on ui, utils) | |
| 219 | + | 14. `app.js`: populates `BB.app`, auto-calls `init()` on DOMContentLoaded | |
| 220 | 220 | ||
| 221 | 221 | Note: items.js (9) references `BB.detail` at call time (not load time), so the forward reference to detail.js (10) resolves by the time `init()` runs. | |
| 222 | 222 |
| @@ -55,13 +55,13 @@ See "Config Schema Field Types" below for all supported `field_type` values. | |||
| 55 | 55 | The core function. Called by the runtime to retrieve feed items. | |
| 56 | 56 | ||
| 57 | 57 | **Parameters:** | |
| 58 | - | - `config` -- A map containing the user's configuration values (keyed by the field `key` from the schema). Also contains a `feeds` array if additional feed URLs were configured. | |
| 59 | - | - `cursor` -- A string for pagination (the `next_cursor` from a previous fetch), or `()` (Rhai's unit/nil) on the first call. | |
| 58 | + | - `config`: A map containing the user's configuration values (keyed by the field `key` from the schema). Also contains a `feeds` array if additional feed URLs were configured. | |
| 59 | + | - `cursor`: A string for pagination (the `next_cursor` from a previous fetch), or `()` (Rhai's unit/nil) on the first call. | |
| 60 | 60 | ||
| 61 | 61 | **Must return a map with:** | |
| 62 | - | - `items` -- An array of item maps (see "Item Structure" below) | |
| 63 | - | - `has_more` -- Boolean indicating whether more pages are available | |
| 64 | - | - `next_cursor` -- (Optional) String cursor for the next page | |
| 62 | + | - `items`: An array of item maps (see "Item Structure" below) | |
| 63 | + | - `has_more`: Boolean indicating whether more pages are available | |
| 64 | + | - `next_cursor`: (Optional) String cursor for the next page | |
| 65 | 65 | ||
| 66 | 66 | ```rhai | |
| 67 | 67 | fn fetch(config, cursor) { | |
| @@ -239,8 +239,8 @@ Each field in the `fields` array supports these properties: | |||
| 239 | 239 | ||
| 240 | 240 | The Rhai engine enforces these limits per plugin execution: | |
| 241 | 241 | ||
| 242 | - | - **Max operations:** 100,000 -- Caps total operations per script call. A typical RSS fetch uses 1,000-5,000 operations. This limit catches infinite loops while allowing complex plugins. | |
| 243 | - | - **Max expression depth:** 128 -- Limits AST nesting depth for both expressions and function calls, preventing stack overflows from deeply recursive scripts. | |
| 242 | + | - **Max operations:** 100,000. Caps total operations per script call. A typical RSS fetch uses 1,000-5,000 operations. This limit catches infinite loops while allowing complex plugins. | |
| 243 | + | - **Max expression depth:** 128. Limits AST nesting depth for both expressions and function calls, preventing stack overflows from deeply recursive scripts. | |
| 244 | 244 | ||
| 245 | 245 | If a plugin exceeds either limit, the engine terminates execution and returns an error. | |
| 246 | 246 | ||
| @@ -316,7 +316,7 @@ The built-in plugins serve as working examples: | |||
| 316 | 316 | | `hackernews.rhai` | 228 | `http_get_json` for API calls, pagination with cursors, score metadata | | |
| 317 | 317 | | `arxiv.rhai` | 179 | `parse_xml` for custom XML, `select` config field, category filtering | | |
| 318 | 318 | ||
| 319 | - | `rss.rhai` is the canonical example -- it covers the most common pattern (fetch XML, parse with `parse_feed`, map entries to items) in under 150 lines. | |
| 319 | + | `rss.rhai` is the canonical example, it covers the most common pattern (fetch XML, parse with `parse_feed`, map entries to items) in under 150 lines. | |
| 320 | 320 | ||
| 321 | 321 | ## Plugin File Location | |
| 322 | 322 |