[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6C57X4C3TQSWZJC882ECQV2":3,"skill-same:01M6C57X4C3TQSWZJC882ECQV2":183},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":21,"linkOnly":22,"markdownContent":23,"markdownBody":24,"frontMatter":25,"refName":26,"commitSha":27,"skillDirectory":28,"forksCount":13,"repositoryHtmlUrl":29,"repositoryHomepageUrl":30,"ownerHtmlUrl":31,"ownerBlogUrl":32,"ownerDescription":33,"updatedAt":34,"install":35,"resources":48},"01M6C57X4C3TQSWZJC882ECQV2","performance-observability",null,"ditto-examples\u002Fditto-cookbook","ditto-examples","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F192611150?v=4","01M6C57F0Y1W6S5D9RD8GHE3YB","Ditto Examples","01M6C57F52Z11DDMBC0ZJ7FJPG",0,"MIT","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002FSKILL.md",[],[18,19,20],"performance","observability","performance observability","2026-11-10 10:12:30",false,"---\nname: performance-observability\ndescription: Ditto SDK store observer performance and diagnostics (Flutter, JavaScript, Kotlin, Swift): observer lifecycle, backpressure (registerObserverV2, signalNext, suspending handlers), narrow UI updates, differs, redundant writes, indexes with ADVISE, EXPLAIN, and PROFILE, the SDK logger, and system: collections. Use when a Ditto query is slow, a screen janks or memory grows with observers, upserts repeat on refresh, or when setting up Ditto logging.\n---\n\n# Ditto Performance and Observability\n\nKeeps Ditto apps responsive and diagnosable: observers and backpressure, narrow UI updates, redundant writes, indexes, logging, and diagnostics. Targets Ditto SDK 5.2.0 in every SDK; DQL examples are SDK-neutral, API examples are per SDK.\n\n## Before You Apply\n\n- Identify the project's SDK and version: Flutter (`ditto_live` in `pubspec.yaml` \u002F `pubspec.lock`), JavaScript (`@dittolive\u002Fditto` in `package.json`), Kotlin (`com.ditto:ditto-kotlin` in Gradle files), or Swift (`DittoSwift` in `Package.swift`, `Package.resolved`, or the Xcode project). These rules target SDK 5.2.0; for devices that still run 5.1.0, also apply `§ Known Issues in SDK 5.1.0`. **Note (SDK 5.1.0 and 5.2.0)** marks easy-to-miss behavior confirmed on both releases, **Note (SDK 5.1.0)** behavior seen on 5.1.0 only; each gives the safe pattern. **(SDK 5.1+)** \u002F **(SDK 5.2+)** mark features introduced in that release.\n- Read that SDK's page, `..\u002Fguide\u002Freference\u002Fbest-practices\u002Fplatforms\u002F\u003Csdk>.md` (`flutter`, `javascript`, `kotlin`, `swift`), for the API calls; its sections are named `\u003CTopic> in \u003CSDK>`, such as *Backpressure in Swift*. DQL examples are `examples\u002F*.sql` (every SDK); API examples are in `examples\u002F\u003Csdk>\u002F` with the same base name in every SDK.\n- `§ \u003CHeading>` cites the full guide: Grep the heading in `..\u002Fguide\u002Freference\u002Fbest-practices\u002F` (including `platforms\u002F`) and `..\u002Fguide\u002Freference\u002Fknowledge\u002F`, then read that section.\n\n**(Experimental)** marks APIs that the SDK marks as experimental (Flutter `registerObserverV2`, `registerObserverWithSignalNext`, `StoreObserverV2`).\n\n## Prevents\n\n- Unbounded memory growth from Flutter observers registered with `onChange` whose `changes` stream is never consumed (SDK 5.1.0 and 5.2.0)\n- Leaked observers (never released, registered in render code, or left to garbage collection)\n- Slow or async work in default observer callbacks without backpressure\n- Observers that stop updating because `signalNext()` is never called\n- Full-screen updates from one broad observer at the root of a screen\n- Redundant writes (`DO UPDATE` re-upserts, whole-document rewrites) that mark documents mutated and fire observers\n- Collection scans from missing or unusable indexes, and strict mode disabling index scans\n- A logger configured after Ditto opens, and lost log forwarding\n\n## Workflow\n\nAdding a screen that shows live data:\n\n```\n- [ ] Subscription exists at app or feature scope (observers do not sync data)\n- [ ] Observer registered once by the screen's owner (not in render code), with ORDER BY\n- [ ] Results consumed in one place and copied into plain values\n- [ ] Each region (badge, list, detail) has its own narrow observer\n- [ ] Observer released when its owner goes away\n- [ ] Indexes for the observer's WHERE\u002FORDER BY created at startup\n```\n\nInvestigating a slow query:\n\n```\n- [ ] EXPLAIN: look for scan instead of indexScan on large collections\n- [ ] Check the index usage rules (functions on fields, OR branches, strict mode)\n- [ ] ADVISE (SDK 5.1+) for suggested indexes; add them to startup code\n- [ ] PROFILE: compare documentsIn and documentsOut of filter steps\n- [ ] Narrow the query: WHERE, projection, ORDER BY ... LIMIT\n- [ ] During development, lower DQL_SLOW_REQUEST_WARN_SECONDS (SDK 5.1+) in the system parameters of DittoConfig\n```\n\n## Rules\n\n### 1. Consume observer results in one place and copy them out (CRITICAL)\n\nCopy plain values (or your own model objects) out of each result and keep no result or row objects. The `query-sync` skill owns the full observer pattern.\n\n| SDK | Consume results | Pitfalls |\n|---|---|---|\n| Flutter | `registerObserver` **without** `onChange`; exactly one `StreamSubscription` on `changes` (or one mounted `StreamBuilder`) | `changes` is single-subscription: a second `listen()` throws a `StateError` |\n| JavaScript | A synchronous handler passed to `registerObserver` | An `async` handler is not awaited, so updates overlap |\n| Kotlin | Collect `observe(...)` in the owner's scope, or a suspending handler | Released with `close()` (or by cancelling the collecting coroutine), not `cancel()` |\n| Swift | The `handler:` of `registerObserver` | Runs on the main queue unless you pass `deliverOn:` |\n\n> **Note (SDK 5.1.0 and 5.2.0):** In Flutter, when an observer is registered with `onChange`, every result is **also** queued in its `changes` stream. If nothing listens to `changes`, those queued results stay in memory for the lifetime of the observer, so memory use grows with every update. The same applies to `registerObserverV2`, which starts observing as soon as it is registered, with or without `onChange`: listen to its `changes` stream right after registering it. If you need `onChange`, also drain the stream, for example with `observer.changes.listen((_) {})`.\n\n**Detection**: Flutter `onChange:` with no `.changes.listen` \u002F `await for` on the same observer; result or row objects in state or caches; displayed lists without `ORDER BY`.\n\n**✅ DO**: add `ORDER BY` whenever result order matters, with `_id` as a tie-breaker (otherwise the order is not guaranteed).\n**❌ DON'T**: keep result or row objects across callbacks.\n\n`§ Observer Lifecycle`, `§ Store Observers in Flutter`, `§ Stable ordering`, `§ Working with Query Results` · Example: `observer-performance` in `examples\u002F\u003Csdk>\u002F`\n\n### 2. Release every observer (CRITICAL)\n\nAn observer runs until it is released: `cancel()` in Flutter, JavaScript, and Swift, `close()` in Kotlin (or cancel the collecting coroutine). Do not rely on garbage collection (in JavaScript, in testing, garbage-collected observers kept running) or on closing Ditto. In Flutter, cancelling the `StreamSubscription` does not cancel a `StoreObserver`, and `await ditto.close()` does not close the `changes` stream (`cancel()` does nothing once Ditto is closed): cancel both in `dispose()`, before closing Ditto.\n\n**✅ DO**: register observers in the owner's start hook (Flutter `initState()`, a view model, a service), never in render code; let observers follow screen scope and keep subscriptions at app or feature scope.\n**❌ DON'T**: create an observer per list item (observe the list once and pass values down); rely on closing Ditto to end a loop over observer results.\n\n`§ Observer Lifecycle`, `§ Observer lifecycle in Flutter`, `§ Resource Cleanup and Shutdown` · Facts: [reference](reference\u002Foptimization-patterns.md#observer-lifecycle) · Examples: `state-management-good`, `state-management-bad` in `examples\u002F\u003Csdk>\u002F`\n\n### 3. Keep default observer callbacks fast (HIGH)\n\nWithout backpressure, Ditto does not wait for your code before delivering the next result: in Flutter, pausing `changes` of a `StoreObserver` only queues results; in JavaScript, `async` handlers overlap and an older result can finish last.\n\n**Detection**: an `async` callback that `await`s network, file, or database work; a callback that writes to its own collection without a guard.\n\n**✅ DO**: keep the callback synchronous and short (copy values, map to models, update UI state); move expensive computation off the UI thread on copied plain values; throttle UI updates for very busy collections; use the backpressure APIs (rule 4) for slow or asynchronous work per update.\n\n**❌ DON'T**: wait for slow work inside a default callback; write to the same collection from inside its observer without a guard (each write triggers the observer again).\n\n`§ Keep observer callbacks fast` · Example: `observer-performance` in `examples\u002F\u003Csdk>\u002F`\n\n### 4. Use backpressure for slow or async work (HIGH)\n\nWhile your code is busy, Ditto holds back further updates and later delivers the latest state, so intermediate results are merged instead of queued (SDK 5.1+).\n\n| Situation | Flutter | JavaScript | Kotlin | Swift |\n|---|---|---|---|---|\n| UI state | `registerObserver` + `changes` | `registerObserver` | `observe(...)` | `registerObserver(... handler:)` |\n| Slow or `async` work per update | `registerObserverV2` + `await for` **(Experimental)** | `registerObserverWithSignalNext` | Suspending handler, or `collect` | `handlerWithSignalNext:`, with `deliverOn:` |\n| Work that finishes elsewhere | `registerObserverWithSignalNext` **(Experimental)** | `registerObserverWithSignalNext` | Suspend until done | `handlerWithSignalNext:` |\n| Only the latest state matters | `registerObserverV2` with a slow consumer | Throttle | `observe(...).conflate()` | Throttle |\n\nWith `signalNext`, the observer delivers one result and then waits. **If you never call it, the observer stops delivering updates.** Call it in `finally` (or `defer`) so an error does not stall the observer. In Flutter, do not pause or resume the `changes` stream of such an observer; `signalNext()` has no effect on `registerObserverV2` observers.\n\n`§ Backpressure (SDK 5.1+)`, `§ Choosing an observer API`, `§ Backpressure in Flutter` · Example: `observer-backpressure` in `examples\u002F\u003Csdk>\u002F` · Flutter `await for` code: [reference](reference\u002Foptimization-patterns.md#backpressure-example-registerobserverv2)\n\n### 5. Keep UI updates partial (HIGH)\n\nAn observer delivers a new result for **any** change that affects its query. Updating state at the top of a large screen renders the whole screen again, which can drop frames and lose scroll position or input focus.\n\n**Detection**: one whole-collection observer at the root of a screen; a badge that loads the full list to count it.\n\n**✅ DO**:\n- Give each screen region its own small observer, owned by the component that shows it\n- Use lazy lists that render only visible rows, with a row identity of `_id`\n- Use aggregate queries for summary components (`COUNT(*)` for a badge), and skip the update when the value is unchanged\n- With a state management library, let one store, provider, or view model own each observer and release it in its dispose hook\n- Map results to immutable models that compare by content if you rely on equality to skip updates\n- Use a differ only where you need to know what changed (for example an animated list)\n\n**❌ DON'T**: register an observer per list row (60+ stall on 5.1.0); update the entire screen from one root observer on every change; diff large or unbounded result sets (the differ keeps the previous result in memory; bound diffed queries with `LIMIT`).\n\nDiffer index semantics: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#differ).\n\n`§ Partial UI Updates`, `§ Diffing Results` · Example: `partial-ui-updates` in `examples\u002F\u003Csdk>\u002F`\n\n### 6. Avoid unnecessary writes (HIGH)\n\n`ON ID CONFLICT DO UPDATE` rewrites every supplied field even when unchanged, so the document is reported as mutated and observers can fire. `DO UPDATE_LOCAL_DIFF` writes only differing fields (nothing when nothing changed). `UPDATE ... SET f = \u003Ccurrent value>` is still a mutation, appears in the result's mutated document IDs, and can wake observers.\n\n**Detection**: periodic re-imports with `DO UPDATE`; read-modify-write of a whole document; `UPDATE` without a condition that skips documents already in the target state.\n\n```sql\n-- Re-upserting unchanged data is a no-op\nINSERT INTO products DOCUMENTS (:product) ON ID CONFLICT DO UPDATE_LOCAL_DIFF\n\n-- Field-level update that skips documents already in the target state\n-- (:none is a sentinel that never equals a real status, for example '')\nUPDATE orders SET status = :status WHERE _id = :id AND coalesce(status, :none) != :status\n```\n\n**✅ DO**: use `DO UPDATE_LOCAL_DIFF` for upserts and re-imports; `SET` only changed fields, nested ones individually (`SET address.city = :city`); use `coalesce` in skip conditions so missing or `null` fields stay eligible; check the result's mutated document IDs to see whether anything was written.\n**❌ DON'T**: read a document, modify it in app code, and write the whole document back with `DO UPDATE` or `DO UPDATE_LOCAL_DIFF` (stale values are written back); expect `DO UPDATE` or `SET obj = {...}` to replace an object (in default strict mode they merge; remove keys with `UNSET`).\n\n`§ ON ID CONFLICT`, `§ Prefer field-level updates over whole-document rewrites`, `§ Assigning an object merges it` · Examples: [examples\u002Funnecessary-deltas-good.sql](examples\u002Funnecessary-deltas-good.sql), [examples\u002Funnecessary-deltas-bad.sql](examples\u002Funnecessary-deltas-bad.sql)\n\n### 7. Create the indexes your queries need (HIGH)\n\nIndexes are local to each device and persist across restarts. They are used by `execute` and store observers (not by subscriptions), and in-memory stores (Flutter Web, the JavaScript SDK in browsers) do not support them.\n\n```sql\n-- Composite index (SDK 5.1+): equality field first, then the range or sort field\nCREATE INDEX IF NOT EXISTS idx_orders_customer_createdAt ON orders (customerId, createdAt DESC)\n\n-- Index-friendly: equality on the leading key, range and sort on the second key\nSELECT * FROM orders WHERE customerId = :customerId AND createdAt >= :since ORDER BY createdAt DESC\n```\n\n- Index scans: `=`, `IN :values`, ranges, prefix `LIKE 'abc%'`, `!=`, `IS MISSING`\n- **Collection scans**: any function applied to the field (`lower(name) = ...`, `starts_with(...)`, `coalesce(isDeleted, false) = false`), element lookups (`array_contains(tags, 'x')`), and `OR` when any branch is not indexed\n- Match the sort direction of a composite index to avoid an extra sort step. On SDK 5.1.0, keep all keys in one direction (`(customerId, createdAt)`), also for `ADVISE` suggestions: mixed `ASC`\u002F`DESC` keys return wrong order there (`§ Known Issues in SDK 5.1.0`)\n- With `DQL_STRICT_MODE` set to `true`, the planner of SDK 5.1.0 and 5.2.0 does **not** use secondary indexes (ID lookups and full-collection `COUNT(*)` are not affected, and `ADVISE` returns no suggestions); keep the default (`false`) if you rely on indexes\n- Keep a field's type declarations consistent: per Ditto's docs only its latest CRDT type is indexed (mixed REGISTER\u002FMAP writes risk wrong results), and other declarations read other values\n\n**✅ DO**: create indexes at startup with `CREATE INDEX IF NOT EXISTS`, after Ditto opens and before queries and observers start; run `ADVISE` (SDK 5.1+) for important queries during development and copy the suggested statements into startup code; use `EXPLAIN` to check the access path and `PROFILE` to measure.\n**❌ DON'T**: measure with `EXPLAIN` (it never runs the query); run `ADVISE AND PROVISION` in production (it creates indexes); index every field \"just in case\" (each costs write time and storage); expect `IF NOT EXISTS` to update a definition (it checks only the name).\n\nIndex usage table, `EXPLAIN`\u002F`PROFILE` output, directives, query scope: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#index-usage-rules).\n\n`§ Indexing and Query Performance`, `§ Index Usage Rules`, `§ ADVISE (SDK 5.1+)`, `§ EXPLAIN and PROFILE` · Example: [examples\u002Findexing-query-performance.sql](examples\u002Findexing-query-performance.sql)\n\n### 8. Configure logging before opening Ditto (HIGH)\n\nConfigure the logger before Ditto opens, so startup is logged with your settings.\n\n| | Flutter | JavaScript | Kotlin | Swift |\n|---|---|---|---|---|\n| Logger | `DittoLogger` (after `await Ditto.init()`; it throws before) | `Logger` | `DittoLogger` | `DittoLogger` |\n| Forward events | `customLogCallback` (reset by `ditto.close()`: set it again before every open) | `setCustomLogCallback` | `observeLogEvents()` (a `Flow`) | `setCustomLogCallback(_:)` |\n| Export on-disk logs | `exportLogs(path)` | `exportToFile(path)` | `exportToFile(path)` | `export(to:)` |\n\n- Levels: `error`, `warning`, `info`, `debug`, `verbose`; default `info`. Use `warning` in production, `debug` while debugging; `verbose` multiplies the log output.\n- The export fails if the file exists or its directory does not; use a fresh, timestamped `.jsonl.gz` path. On-disk log details: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#logging-details).\n\n**❌ DON'T**: leave `verbose` enabled in production; change the `ROTATING_LOG_FILE_*` parameters unless Ditto support advises otherwise.\n\n`§ Logging`, `§ Configuring logging`, `§ Logging in Flutter` · Examples: `logging-configuration-good`, `logging-configuration-bad` in `examples\u002F\u003Csdk>\u002F`\n\n### 9. Query system: collections on demand, not with long-lived observers (MEDIUM)\n\n`system:` virtual collections (`system:system_info`, `system:indexes`, `system:data_sync_info`, `system:active_requests`, and others) are local, read only, and snapshot-based. Query them with `execute` when needed.\n\n**✅ DO**: for live sync status, use one observer with a trivial callback; pass diagnostic parameters in the system parameters of `DittoConfig` on every open (not persisted; on SDK 5.1, `ALTER SYSTEM` after open, before sync) and use `ALTER SYSTEM` for run-time changes such as a support session; include `system:sync_degradations` (SDK 5.2+) in diagnostics; create support bundles with `EXECUTE FUNCTION system_request_support_bundle()` (SDK 5.2+).\n**❌ DON'T**: register long-lived observers on `system:system_info`, or observers on `system:data_sync_info` in many places.\n\nCollections, sample queries, and diagnostic parameters: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#system-virtual-collections).\n\n`§ System Virtual Collections`, `§ Monitoring Sync Status`, `§ System Parameters Reference`, `§ Support bundles (SDK 5.2+)` · Example: `logging-configuration-good` in `examples\u002F\u003Csdk>\u002F`\n\n## Checklist\n\n- [ ] Flutter: no `onChange` without consuming `changes` (SDK 5.1.0 and 5.2.0)\n- [ ] `ORDER BY` on every observer whose order is displayed\n- [ ] Observers registered outside render code, one per region, not per item\n- [ ] Every observer released by its owner (Flutter: `StreamSubscription.cancel()` and `observer.cancel()` in `dispose()`; Kotlin: `close()`, or cancelling the coroutine that collects `observe`)\n- [ ] No slow work inside default observer callbacks\n- [ ] Backpressure APIs for slow work; `signalNext()` in `finally`; experimental APIs labeled **(Experimental)**\n- [ ] Observer result sets bounded with `WHERE` and `LIMIT`; differs only on bounded results\n- [ ] `DO UPDATE_LOCAL_DIFF` for upserts; field-level `UPDATE` with `coalesce` skip conditions\n- [ ] `CREATE INDEX IF NOT EXISTS` at startup (not on in-memory stores)\n- [ ] Composite indexes: equality fields first, matching sort direction; no functions on indexed fields\n- [ ] Strict mode left at `false` when relying on indexes\n- [ ] One `IN :ids` query instead of one query per ID; constant query strings with parameters\n- [ ] Logger configured before Ditto opens; `warning` in production, no `verbose`\n- [ ] Flutter: `customLogCallback` set again before every `Ditto.open()`; log export path is new\n- [ ] No long-lived observers on `system:system_info`\n\n## More\n\n- Reference: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md) - observer APIs and lifecycle in each SDK, differs, index rules, `ADVISE`\u002F`EXPLAIN`\u002F`PROFILE`, query scope, long-running requests, write policies, logging, `system:` collections and parameters\n- Examples: [examples\u002F](examples\u002F) - DQL (`.sql`): unnecessary writes, indexing; `examples\u002F\u003Csdk>\u002F`: observers, backpressure, state management, partial UI updates, logging\n- Related skills: `query-sync` (owns the observer and subscription lifecycle pattern, DQL, upserts), `data-modeling` (field-level updates and CRDT merge behavior), `storage-lifecycle` (deletion, eviction, and storage)\n- Guide: `§ Observing Changes`, `§ Indexing and Query Performance`, `§ Logging and Observability`\n","# Ditto Performance and Observability\n\nKeeps Ditto apps responsive and diagnosable: observers and backpressure, narrow UI updates, redundant writes, indexes, logging, and diagnostics. Targets Ditto SDK 5.2.0 in every SDK; DQL examples are SDK-neutral, API examples are per SDK.\n\n## Before You Apply\n\n- Identify the project's SDK and version: Flutter (`ditto_live` in `pubspec.yaml` \u002F `pubspec.lock`), JavaScript (`@dittolive\u002Fditto` in `package.json`), Kotlin (`com.ditto:ditto-kotlin` in Gradle files), or Swift (`DittoSwift` in `Package.swift`, `Package.resolved`, or the Xcode project). These rules target SDK 5.2.0; for devices that still run 5.1.0, also apply `§ Known Issues in SDK 5.1.0`. **Note (SDK 5.1.0 and 5.2.0)** marks easy-to-miss behavior confirmed on both releases, **Note (SDK 5.1.0)** behavior seen on 5.1.0 only; each gives the safe pattern. **(SDK 5.1+)** \u002F **(SDK 5.2+)** mark features introduced in that release.\n- Read that SDK's page, `..\u002Fguide\u002Freference\u002Fbest-practices\u002Fplatforms\u002F\u003Csdk>.md` (`flutter`, `javascript`, `kotlin`, `swift`), for the API calls; its sections are named `\u003CTopic> in \u003CSDK>`, such as *Backpressure in Swift*. DQL examples are `examples\u002F*.sql` (every SDK); API examples are in `examples\u002F\u003Csdk>\u002F` with the same base name in every SDK.\n- `§ \u003CHeading>` cites the full guide: Grep the heading in `..\u002Fguide\u002Freference\u002Fbest-practices\u002F` (including `platforms\u002F`) and `..\u002Fguide\u002Freference\u002Fknowledge\u002F`, then read that section.\n\n**(Experimental)** marks APIs that the SDK marks as experimental (Flutter `registerObserverV2`, `registerObserverWithSignalNext`, `StoreObserverV2`).\n\n## Prevents\n\n- Unbounded memory growth from Flutter observers registered with `onChange` whose `changes` stream is never consumed (SDK 5.1.0 and 5.2.0)\n- Leaked observers (never released, registered in render code, or left to garbage collection)\n- Slow or async work in default observer callbacks without backpressure\n- Observers that stop updating because `signalNext()` is never called\n- Full-screen updates from one broad observer at the root of a screen\n- Redundant writes (`DO UPDATE` re-upserts, whole-document rewrites) that mark documents mutated and fire observers\n- Collection scans from missing or unusable indexes, and strict mode disabling index scans\n- A logger configured after Ditto opens, and lost log forwarding\n\n## Workflow\n\nAdding a screen that shows live data:\n\n```\n- [ ] Subscription exists at app or feature scope (observers do not sync data)\n- [ ] Observer registered once by the screen's owner (not in render code), with ORDER BY\n- [ ] Results consumed in one place and copied into plain values\n- [ ] Each region (badge, list, detail) has its own narrow observer\n- [ ] Observer released when its owner goes away\n- [ ] Indexes for the observer's WHERE\u002FORDER BY created at startup\n```\n\nInvestigating a slow query:\n\n```\n- [ ] EXPLAIN: look for scan instead of indexScan on large collections\n- [ ] Check the index usage rules (functions on fields, OR branches, strict mode)\n- [ ] ADVISE (SDK 5.1+) for suggested indexes; add them to startup code\n- [ ] PROFILE: compare documentsIn and documentsOut of filter steps\n- [ ] Narrow the query: WHERE, projection, ORDER BY ... LIMIT\n- [ ] During development, lower DQL_SLOW_REQUEST_WARN_SECONDS (SDK 5.1+) in the system parameters of DittoConfig\n```\n\n## Rules\n\n### 1. Consume observer results in one place and copy them out (CRITICAL)\n\nCopy plain values (or your own model objects) out of each result and keep no result or row objects. The `query-sync` skill owns the full observer pattern.\n\n| SDK | Consume results | Pitfalls |\n|---|---|---|\n| Flutter | `registerObserver` **without** `onChange`; exactly one `StreamSubscription` on `changes` (or one mounted `StreamBuilder`) | `changes` is single-subscription: a second `listen()` throws a `StateError` |\n| JavaScript | A synchronous handler passed to `registerObserver` | An `async` handler is not awaited, so updates overlap |\n| Kotlin | Collect `observe(...)` in the owner's scope, or a suspending handler | Released with `close()` (or by cancelling the collecting coroutine), not `cancel()` |\n| Swift | The `handler:` of `registerObserver` | Runs on the main queue unless you pass `deliverOn:` |\n\n> **Note (SDK 5.1.0 and 5.2.0):** In Flutter, when an observer is registered with `onChange`, every result is **also** queued in its `changes` stream. If nothing listens to `changes`, those queued results stay in memory for the lifetime of the observer, so memory use grows with every update. The same applies to `registerObserverV2`, which starts observing as soon as it is registered, with or without `onChange`: listen to its `changes` stream right after registering it. If you need `onChange`, also drain the stream, for example with `observer.changes.listen((_) {})`.\n\n**Detection**: Flutter `onChange:` with no `.changes.listen` \u002F `await for` on the same observer; result or row objects in state or caches; displayed lists without `ORDER BY`.\n\n**✅ DO**: add `ORDER BY` whenever result order matters, with `_id` as a tie-breaker (otherwise the order is not guaranteed).\n**❌ DON'T**: keep result or row objects across callbacks.\n\n`§ Observer Lifecycle`, `§ Store Observers in Flutter`, `§ Stable ordering`, `§ Working with Query Results` · Example: `observer-performance` in `examples\u002F\u003Csdk>\u002F`\n\n### 2. Release every observer (CRITICAL)\n\nAn observer runs until it is released: `cancel()` in Flutter, JavaScript, and Swift, `close()` in Kotlin (or cancel the collecting coroutine). Do not rely on garbage collection (in JavaScript, in testing, garbage-collected observers kept running) or on closing Ditto. In Flutter, cancelling the `StreamSubscription` does not cancel a `StoreObserver`, and `await ditto.close()` does not close the `changes` stream (`cancel()` does nothing once Ditto is closed): cancel both in `dispose()`, before closing Ditto.\n\n**✅ DO**: register observers in the owner's start hook (Flutter `initState()`, a view model, a service), never in render code; let observers follow screen scope and keep subscriptions at app or feature scope.\n**❌ DON'T**: create an observer per list item (observe the list once and pass values down); rely on closing Ditto to end a loop over observer results.\n\n`§ Observer Lifecycle`, `§ Observer lifecycle in Flutter`, `§ Resource Cleanup and Shutdown` · Facts: [reference](reference\u002Foptimization-patterns.md#observer-lifecycle) · Examples: `state-management-good`, `state-management-bad` in `examples\u002F\u003Csdk>\u002F`\n\n### 3. Keep default observer callbacks fast (HIGH)\n\nWithout backpressure, Ditto does not wait for your code before delivering the next result: in Flutter, pausing `changes` of a `StoreObserver` only queues results; in JavaScript, `async` handlers overlap and an older result can finish last.\n\n**Detection**: an `async` callback that `await`s network, file, or database work; a callback that writes to its own collection without a guard.\n\n**✅ DO**: keep the callback synchronous and short (copy values, map to models, update UI state); move expensive computation off the UI thread on copied plain values; throttle UI updates for very busy collections; use the backpressure APIs (rule 4) for slow or asynchronous work per update.\n\n**❌ DON'T**: wait for slow work inside a default callback; write to the same collection from inside its observer without a guard (each write triggers the observer again).\n\n`§ Keep observer callbacks fast` · Example: `observer-performance` in `examples\u002F\u003Csdk>\u002F`\n\n### 4. Use backpressure for slow or async work (HIGH)\n\nWhile your code is busy, Ditto holds back further updates and later delivers the latest state, so intermediate results are merged instead of queued (SDK 5.1+).\n\n| Situation | Flutter | JavaScript | Kotlin | Swift |\n|---|---|---|---|---|\n| UI state | `registerObserver` + `changes` | `registerObserver` | `observe(...)` | `registerObserver(... handler:)` |\n| Slow or `async` work per update | `registerObserverV2` + `await for` **(Experimental)** | `registerObserverWithSignalNext` | Suspending handler, or `collect` | `handlerWithSignalNext:`, with `deliverOn:` |\n| Work that finishes elsewhere | `registerObserverWithSignalNext` **(Experimental)** | `registerObserverWithSignalNext` | Suspend until done | `handlerWithSignalNext:` |\n| Only the latest state matters | `registerObserverV2` with a slow consumer | Throttle | `observe(...).conflate()` | Throttle |\n\nWith `signalNext`, the observer delivers one result and then waits. **If you never call it, the observer stops delivering updates.** Call it in `finally` (or `defer`) so an error does not stall the observer. In Flutter, do not pause or resume the `changes` stream of such an observer; `signalNext()` has no effect on `registerObserverV2` observers.\n\n`§ Backpressure (SDK 5.1+)`, `§ Choosing an observer API`, `§ Backpressure in Flutter` · Example: `observer-backpressure` in `examples\u002F\u003Csdk>\u002F` · Flutter `await for` code: [reference](reference\u002Foptimization-patterns.md#backpressure-example-registerobserverv2)\n\n### 5. Keep UI updates partial (HIGH)\n\nAn observer delivers a new result for **any** change that affects its query. Updating state at the top of a large screen renders the whole screen again, which can drop frames and lose scroll position or input focus.\n\n**Detection**: one whole-collection observer at the root of a screen; a badge that loads the full list to count it.\n\n**✅ DO**:\n- Give each screen region its own small observer, owned by the component that shows it\n- Use lazy lists that render only visible rows, with a row identity of `_id`\n- Use aggregate queries for summary components (`COUNT(*)` for a badge), and skip the update when the value is unchanged\n- With a state management library, let one store, provider, or view model own each observer and release it in its dispose hook\n- Map results to immutable models that compare by content if you rely on equality to skip updates\n- Use a differ only where you need to know what changed (for example an animated list)\n\n**❌ DON'T**: register an observer per list row (60+ stall on 5.1.0); update the entire screen from one root observer on every change; diff large or unbounded result sets (the differ keeps the previous result in memory; bound diffed queries with `LIMIT`).\n\nDiffer index semantics: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#differ).\n\n`§ Partial UI Updates`, `§ Diffing Results` · Example: `partial-ui-updates` in `examples\u002F\u003Csdk>\u002F`\n\n### 6. Avoid unnecessary writes (HIGH)\n\n`ON ID CONFLICT DO UPDATE` rewrites every supplied field even when unchanged, so the document is reported as mutated and observers can fire. `DO UPDATE_LOCAL_DIFF` writes only differing fields (nothing when nothing changed). `UPDATE ... SET f = \u003Ccurrent value>` is still a mutation, appears in the result's mutated document IDs, and can wake observers.\n\n**Detection**: periodic re-imports with `DO UPDATE`; read-modify-write of a whole document; `UPDATE` without a condition that skips documents already in the target state.\n\n```sql\n-- Re-upserting unchanged data is a no-op\nINSERT INTO products DOCUMENTS (:product) ON ID CONFLICT DO UPDATE_LOCAL_DIFF\n\n-- Field-level update that skips documents already in the target state\n-- (:none is a sentinel that never equals a real status, for example '')\nUPDATE orders SET status = :status WHERE _id = :id AND coalesce(status, :none) != :status\n```\n\n**✅ DO**: use `DO UPDATE_LOCAL_DIFF` for upserts and re-imports; `SET` only changed fields, nested ones individually (`SET address.city = :city`); use `coalesce` in skip conditions so missing or `null` fields stay eligible; check the result's mutated document IDs to see whether anything was written.\n**❌ DON'T**: read a document, modify it in app code, and write the whole document back with `DO UPDATE` or `DO UPDATE_LOCAL_DIFF` (stale values are written back); expect `DO UPDATE` or `SET obj = {...}` to replace an object (in default strict mode they merge; remove keys with `UNSET`).\n\n`§ ON ID CONFLICT`, `§ Prefer field-level updates over whole-document rewrites`, `§ Assigning an object merges it` · Examples: [examples\u002Funnecessary-deltas-good.sql](examples\u002Funnecessary-deltas-good.sql), [examples\u002Funnecessary-deltas-bad.sql](examples\u002Funnecessary-deltas-bad.sql)\n\n### 7. Create the indexes your queries need (HIGH)\n\nIndexes are local to each device and persist across restarts. They are used by `execute` and store observers (not by subscriptions), and in-memory stores (Flutter Web, the JavaScript SDK in browsers) do not support them.\n\n```sql\n-- Composite index (SDK 5.1+): equality field first, then the range or sort field\nCREATE INDEX IF NOT EXISTS idx_orders_customer_createdAt ON orders (customerId, createdAt DESC)\n\n-- Index-friendly: equality on the leading key, range and sort on the second key\nSELECT * FROM orders WHERE customerId = :customerId AND createdAt >= :since ORDER BY createdAt DESC\n```\n\n- Index scans: `=`, `IN :values`, ranges, prefix `LIKE 'abc%'`, `!=`, `IS MISSING`\n- **Collection scans**: any function applied to the field (`lower(name) = ...`, `starts_with(...)`, `coalesce(isDeleted, false) = false`), element lookups (`array_contains(tags, 'x')`), and `OR` when any branch is not indexed\n- Match the sort direction of a composite index to avoid an extra sort step. On SDK 5.1.0, keep all keys in one direction (`(customerId, createdAt)`), also for `ADVISE` suggestions: mixed `ASC`\u002F`DESC` keys return wrong order there (`§ Known Issues in SDK 5.1.0`)\n- With `DQL_STRICT_MODE` set to `true`, the planner of SDK 5.1.0 and 5.2.0 does **not** use secondary indexes (ID lookups and full-collection `COUNT(*)` are not affected, and `ADVISE` returns no suggestions); keep the default (`false`) if you rely on indexes\n- Keep a field's type declarations consistent: per Ditto's docs only its latest CRDT type is indexed (mixed REGISTER\u002FMAP writes risk wrong results), and other declarations read other values\n\n**✅ DO**: create indexes at startup with `CREATE INDEX IF NOT EXISTS`, after Ditto opens and before queries and observers start; run `ADVISE` (SDK 5.1+) for important queries during development and copy the suggested statements into startup code; use `EXPLAIN` to check the access path and `PROFILE` to measure.\n**❌ DON'T**: measure with `EXPLAIN` (it never runs the query); run `ADVISE AND PROVISION` in production (it creates indexes); index every field \"just in case\" (each costs write time and storage); expect `IF NOT EXISTS` to update a definition (it checks only the name).\n\nIndex usage table, `EXPLAIN`\u002F`PROFILE` output, directives, query scope: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#index-usage-rules).\n\n`§ Indexing and Query Performance`, `§ Index Usage Rules`, `§ ADVISE (SDK 5.1+)`, `§ EXPLAIN and PROFILE` · Example: [examples\u002Findexing-query-performance.sql](examples\u002Findexing-query-performance.sql)\n\n### 8. Configure logging before opening Ditto (HIGH)\n\nConfigure the logger before Ditto opens, so startup is logged with your settings.\n\n| | Flutter | JavaScript | Kotlin | Swift |\n|---|---|---|---|---|\n| Logger | `DittoLogger` (after `await Ditto.init()`; it throws before) | `Logger` | `DittoLogger` | `DittoLogger` |\n| Forward events | `customLogCallback` (reset by `ditto.close()`: set it again before every open) | `setCustomLogCallback` | `observeLogEvents()` (a `Flow`) | `setCustomLogCallback(_:)` |\n| Export on-disk logs | `exportLogs(path)` | `exportToFile(path)` | `exportToFile(path)` | `export(to:)` |\n\n- Levels: `error`, `warning`, `info`, `debug`, `verbose`; default `info`. Use `warning` in production, `debug` while debugging; `verbose` multiplies the log output.\n- The export fails if the file exists or its directory does not; use a fresh, timestamped `.jsonl.gz` path. On-disk log details: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#logging-details).\n\n**❌ DON'T**: leave `verbose` enabled in production; change the `ROTATING_LOG_FILE_*` parameters unless Ditto support advises otherwise.\n\n`§ Logging`, `§ Configuring logging`, `§ Logging in Flutter` · Examples: `logging-configuration-good`, `logging-configuration-bad` in `examples\u002F\u003Csdk>\u002F`\n\n### 9. Query system: collections on demand, not with long-lived observers (MEDIUM)\n\n`system:` virtual collections (`system:system_info`, `system:indexes`, `system:data_sync_info`, `system:active_requests`, and others) are local, read only, and snapshot-based. Query them with `execute` when needed.\n\n**✅ DO**: for live sync status, use one observer with a trivial callback; pass diagnostic parameters in the system parameters of `DittoConfig` on every open (not persisted; on SDK 5.1, `ALTER SYSTEM` after open, before sync) and use `ALTER SYSTEM` for run-time changes such as a support session; include `system:sync_degradations` (SDK 5.2+) in diagnostics; create support bundles with `EXECUTE FUNCTION system_request_support_bundle()` (SDK 5.2+).\n**❌ DON'T**: register long-lived observers on `system:system_info`, or observers on `system:data_sync_info` in many places.\n\nCollections, sample queries, and diagnostic parameters: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md#system-virtual-collections).\n\n`§ System Virtual Collections`, `§ Monitoring Sync Status`, `§ System Parameters Reference`, `§ Support bundles (SDK 5.2+)` · Example: `logging-configuration-good` in `examples\u002F\u003Csdk>\u002F`\n\n## Checklist\n\n- [ ] Flutter: no `onChange` without consuming `changes` (SDK 5.1.0 and 5.2.0)\n- [ ] `ORDER BY` on every observer whose order is displayed\n- [ ] Observers registered outside render code, one per region, not per item\n- [ ] Every observer released by its owner (Flutter: `StreamSubscription.cancel()` and `observer.cancel()` in `dispose()`; Kotlin: `close()`, or cancelling the coroutine that collects `observe`)\n- [ ] No slow work inside default observer callbacks\n- [ ] Backpressure APIs for slow work; `signalNext()` in `finally`; experimental APIs labeled **(Experimental)**\n- [ ] Observer result sets bounded with `WHERE` and `LIMIT`; differs only on bounded results\n- [ ] `DO UPDATE_LOCAL_DIFF` for upserts; field-level `UPDATE` with `coalesce` skip conditions\n- [ ] `CREATE INDEX IF NOT EXISTS` at startup (not on in-memory stores)\n- [ ] Composite indexes: equality fields first, matching sort direction; no functions on indexed fields\n- [ ] Strict mode left at `false` when relying on indexes\n- [ ] One `IN :ids` query instead of one query per ID; constant query strings with parameters\n- [ ] Logger configured before Ditto opens; `warning` in production, no `verbose`\n- [ ] Flutter: `customLogCallback` set again before every `Ditto.open()`; log export path is new\n- [ ] No long-lived observers on `system:system_info`\n\n## More\n\n- Reference: [reference\u002Foptimization-patterns.md](reference\u002Foptimization-patterns.md) - observer APIs and lifecycle in each SDK, differs, index rules, `ADVISE`\u002F`EXPLAIN`\u002F`PROFILE`, query scope, long-running requests, write policies, logging, `system:` collections and parameters\n- Examples: [examples\u002F](examples\u002F) - DQL (`.sql`): unnecessary writes, indexing; `examples\u002F\u003Csdk>\u002F`: observers, backpressure, state management, partial UI updates, logging\n- Related skills: `query-sync` (owns the observer and subscription lifecycle pattern, DQL, upserts), `data-modeling` (field-level updates and CRDT merge behavior), `storage-lifecycle` (deletion, eviction, and storage)\n- Guide: `§ Observing Changes`, `§ Indexing and Query Performance`, `§ Logging and Observability`\n",{},"main","296051dbcacd4db48c774e072020327b4c18b7d4","plugins\u002Fditto\u002Fskills\u002Fperformance-observability","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook","https:\u002F\u002Fdocs.ditto.live\u002Fhome\u002Fintroduction","https:\u002F\u002Fgithub.com\u002Fditto-examples","https:\u002F\u002Fdocs.ditto.live","Example Apps and Information for Ditto","2026-11-11 15:52:43",{"source":7,"skillName":5,"commands":36,"prompt":46,"zipAvailable":47},[37,40,43],{"tool":38,"command":39},"npx","npx skills add ditto-examples\u002Fditto-cookbook --skill performance-observability",{"tool":41,"command":42},"bunx","bunx skills add ditto-examples\u002Fditto-cookbook --skill performance-observability",{"tool":44,"command":45},"pnpm","pnpm dlx skills add ditto-examples\u002Fditto-cookbook --skill performance-observability","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002FSKILL.md",true,[49,55,59,63,67,71,75,79,83,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,169,173,177],{"path":50,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":53,"sourceHtmlUrl":54},"examples\u002Fflutter\u002Flogging-configuration-bad.dart","OTHER","其他",6301,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Flogging-configuration-bad.dart",{"path":56,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":57,"sourceHtmlUrl":58},"examples\u002Fflutter\u002Flogging-configuration-good.dart",11007,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Flogging-configuration-good.dart",{"path":60,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":61,"sourceHtmlUrl":62},"examples\u002Fflutter\u002Fobserver-backpressure.dart",10054,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Fobserver-backpressure.dart",{"path":64,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":65,"sourceHtmlUrl":66},"examples\u002Fflutter\u002Fobserver-performance.dart",12345,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Fobserver-performance.dart",{"path":68,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":69,"sourceHtmlUrl":70},"examples\u002Fflutter\u002Fpartial-ui-updates.dart",10839,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Fpartial-ui-updates.dart",{"path":72,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":73,"sourceHtmlUrl":74},"examples\u002Fflutter\u002Fstate-management-bad.dart",8170,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Fstate-management-bad.dart",{"path":76,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":77,"sourceHtmlUrl":78},"examples\u002Fflutter\u002Fstate-management-good.dart",7552,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fflutter\u002Fstate-management-good.dart",{"path":80,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":81,"sourceHtmlUrl":82},"examples\u002Findexing-query-performance.sql",7731,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Findexing-query-performance.sql",{"path":84,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":87,"sourceHtmlUrl":88},"examples\u002Fjavascript\u002Flogging-configuration-bad.js","SCRIPT","脚本",3212,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Flogging-configuration-bad.js",{"path":90,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":91,"sourceHtmlUrl":92},"examples\u002Fjavascript\u002Flogging-configuration-good.js",6257,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Flogging-configuration-good.js",{"path":94,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":95,"sourceHtmlUrl":96},"examples\u002Fjavascript\u002Fobserver-backpressure.js",5258,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Fobserver-backpressure.js",{"path":98,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":99,"sourceHtmlUrl":100},"examples\u002Fjavascript\u002Fobserver-performance.jsx",5252,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Fobserver-performance.jsx",{"path":102,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":103,"sourceHtmlUrl":104},"examples\u002Fjavascript\u002Fpartial-ui-updates.jsx",6043,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Fpartial-ui-updates.jsx",{"path":106,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":107,"sourceHtmlUrl":108},"examples\u002Fjavascript\u002Fstate-management-bad.jsx",3624,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Fstate-management-bad.jsx",{"path":110,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":111,"sourceHtmlUrl":112},"examples\u002Fjavascript\u002Fstate-management-good.jsx",4433,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fjavascript\u002Fstate-management-good.jsx",{"path":114,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":115,"sourceHtmlUrl":116},"examples\u002Fkotlin\u002Flogging-configuration-bad.kt",3641,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Flogging-configuration-bad.kt",{"path":118,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":119,"sourceHtmlUrl":120},"examples\u002Fkotlin\u002Flogging-configuration-good.kt",7046,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Flogging-configuration-good.kt",{"path":122,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":123,"sourceHtmlUrl":124},"examples\u002Fkotlin\u002Fobserver-backpressure.kt",4902,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Fobserver-backpressure.kt",{"path":126,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":127,"sourceHtmlUrl":128},"examples\u002Fkotlin\u002Fobserver-performance.kt",5327,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Fobserver-performance.kt",{"path":130,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":131,"sourceHtmlUrl":132},"examples\u002Fkotlin\u002Fpartial-ui-updates.kt",5350,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Fpartial-ui-updates.kt",{"path":134,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":135,"sourceHtmlUrl":136},"examples\u002Fkotlin\u002Fstate-management-bad.kt",3811,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Fstate-management-bad.kt",{"path":138,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":139,"sourceHtmlUrl":140},"examples\u002Fkotlin\u002Fstate-management-good.kt",4334,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fkotlin\u002Fstate-management-good.kt",{"path":142,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":143,"sourceHtmlUrl":144},"examples\u002Fswift\u002Flogging-configuration-bad.swift",3707,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Flogging-configuration-bad.swift",{"path":146,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":147,"sourceHtmlUrl":148},"examples\u002Fswift\u002Flogging-configuration-good.swift",8213,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Flogging-configuration-good.swift",{"path":150,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":151,"sourceHtmlUrl":152},"examples\u002Fswift\u002Fobserver-backpressure.swift",5570,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Fobserver-backpressure.swift",{"path":154,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":155,"sourceHtmlUrl":156},"examples\u002Fswift\u002Fobserver-performance.swift",7019,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Fobserver-performance.swift",{"path":158,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":159,"sourceHtmlUrl":160},"examples\u002Fswift\u002Fpartial-ui-updates.swift",6500,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Fpartial-ui-updates.swift",{"path":162,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":163,"sourceHtmlUrl":164},"examples\u002Fswift\u002Fstate-management-bad.swift",4422,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Fstate-management-bad.swift",{"path":166,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":167,"sourceHtmlUrl":168},"examples\u002Fswift\u002Fstate-management-good.swift",4451,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Fswift\u002Fstate-management-good.swift",{"path":170,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":171,"sourceHtmlUrl":172},"examples\u002Funnecessary-deltas-bad.sql",4336,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Funnecessary-deltas-bad.sql",{"path":174,"resourceType":51,"resourceTypeDesc":52,"sizeBytes":175,"sourceHtmlUrl":176},"examples\u002Funnecessary-deltas-good.sql",5971,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Fexamples\u002Funnecessary-deltas-good.sql",{"path":178,"resourceType":179,"resourceTypeDesc":180,"sizeBytes":181,"sourceHtmlUrl":182},"reference\u002Foptimization-patterns.md","REFERENCE","参考文档",36614,"https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fperformance-observability\u002Freference\u002Foptimization-patterns.md",[184,193,224,245,253,261],{"skillNo":185,"name":186,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":187,"categories":188,"keywords":189,"publishedAt":21},"01M6C58EAD92WQKNE74549PXEW","transactions-attachments","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Ftransactions-attachments\u002FSKILL.md",[],[190,191,192],"transactions attachments","attachments","transactions",{"skillNo":194,"name":195,"nameZh":6,"description":196,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":197,"categories":198,"keywords":204,"publishedAt":21},"01M6C58B0PQHV5WXMQRP9DEPX1","testing","Ditto SDK testing strategies in Flutter, JavaScript, Kotlin, or Swift: local-store test helper, tests for DQL statements, MISSING\u002FNULL filters, merge-sensitive writes, soft delete, observer lifecycle, subscription rules, and multi-peer sync tests in one process. Use when writing or reviewing tests for code that uses Ditto, setting up a test harness, or reproducing a sync or merge bug.","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Ftesting\u002FSKILL.md",[199],{"categoryNo":200,"categoryName":201,"taxonomyType":202,"taxonomyTypeDesc":203},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[195,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223],"swift","harness","lifecycle","up","for","strategies","test harness","when","testing strategies","tests","missing","bug","writing","filters","code","delete","merge","sync","local",{"skillNo":225,"name":226,"nameZh":6,"description":227,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":228,"categories":229,"keywords":230,"publishedAt":21},"01M6C587HJC1SVAPYEZHJBV1C7","storage-lifecycle","Ditto SDK data removal and local storage management covering DELETE vs soft delete vs EVICT, tombstones and their TTL, husk documents, eviction with complementary subscriptions, retention cleanup, and storage monitoring. Use when writing DELETE or EVICT statements, isDeleted or deletedAt flags, or retention policies, when setting TOMBSTONE_TTL_HOURS, or when deleted data came back or evicted documents sync back.","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fstorage-lifecycle\u002FSKILL.md",[],[207,231,232,223,233,234,212,235,236,237,238,220,239,222,240,217,241,242,243,244],"storage","storage lifecycle","with","subscriptions","ttl","use","ditto","retention","sdk","cleanup","documents","monitoring","back","evict",{"skillNo":246,"name":247,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":248,"categories":249,"keywords":250,"publishedAt":21},"01M6C5845MAD3AEBNSN3BT61WH","sdk-setup","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fsdk-setup\u002FSKILL.md",[],[251,252,239],"sdk setup","setup",{"skillNo":254,"name":255,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":256,"categories":257,"keywords":258,"publishedAt":21},"01M6C580SCSJK0E5Z54AJTV242","query-sync","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fquery-sync\u002FSKILL.md",[],[259,260,222],"query","query sync",{"skillNo":262,"name":263,"nameZh":6,"description":264,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":265,"categories":266,"keywords":267,"publishedAt":21},"01M6C57V1EZKA9B5W1G4F00HAH","guide","Searches the complete Ditto SDK best practices guide (SDK 5.2.0, Flutter, JavaScript, Kotlin, Swift) bundled with this plugin, for answers the focused ditto skills do not cover. Use to look up a Ditto API, DQL keyword or function, system parameter, error message, an SDK's API or behavior, or the reasoning behind a rule, or when a Ditto question spans several topics.","https:\u002F\u002Fgithub.com\u002Fditto-examples\u002Fditto-cookbook\u002Fblob\u002Fmain\u002Fplugins\u002Fditto\u002Fskills\u002Fguide\u002FSKILL.md",[],[263,268,269,270,208,271,236,272,273,233,274,275,276,277,209,237,278,212,279,280],"kotlin","plugin","system","question","best practices","api","error","do","message","complete","flutter","javascript","topics"]