> ## Documentation Index
> Fetch the complete documentation index at: https://cindel.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Reactive Watchers: Dart Streams for Real-Time Updates

> Subscribe to Dart streams that emit after committed Cindel writes. Use object, collection, query, and lazy watchers to drive reactive Flutter UIs.

Cindel watchers are Dart `Stream`s that emit after committed database changes. Instead of manually reloading screens after every write, you subscribe to a watcher and let Cindel push fresh data whenever something relevant changes. Local writes notify watchers directly; external changes are caught through periodic polling (default 50 ms).

## Watcher Types

Choose the watcher that fits how your UI consumes data:

| Watcher         | API                                | Emits             |
| --------------- | ---------------------------------- | ----------------- |
| Object          | `collection.watchObject(id)`       | `T?`              |
| Lazy Object     | `collection.watchObjectLazy(id)`   | `void`            |
| Collection      | `collection.watchCollection()`     | `List<T>`         |
| Lazy Collection | `collection.watchCollectionLazy()` | `void`            |
| Query           | `query.watch()`                    | `List<T>`         |
| Lazy Query      | `query.watchLazy()`                | `void`            |
| Change-set      | `db.watchCollectionChanges(name)`  | `CindelChangeSet` |

**Lazy** variants emit a `void` signal rather than the data itself — use them when you manage your own cache and only need to know that something changed.

## Common Options

Every watcher accepts two common options:

* **`pollInterval`** — how often polling checks for changes that were not delivered directly. Defaults to `Duration(milliseconds: 50)`.
* **`fireImmediately`** — when `true`, the stream emits the current value as soon as you subscribe, before any writes occur.

## Object Watcher

Use `watchObject` to track one typed object by id on a detail or edit screen. The emitted value is `T?` — it is `null` when the object has been deleted or was never written.

```dart theme={null}
final sub = db.todos.watchObject(
  todoId,
  fireImmediately: true,
).listen((todo) {
  if (todo == null) {
    detailState = null;
    return;
  }
  detailState = todo;
});
```

## Collection Watcher

Use `watchCollection` to observe the entire typed collection. This is convenient for small collections or screens that intentionally show every record.

```dart theme={null}
final sub = db.settings.watchCollection().listen((settings) {
  settingsState = settings;
});
```

For large collections, prefer a query watcher with filtering, sorting, and limits so the stream only delivers the slice of data your UI actually renders.

## Query Watcher

Use `query.watch()` to observe a typed query result. Query watchers are the best default for list screens because you can push filtering, sorting, and pagination into the database rather than into widget code.

```dart theme={null}
final sub = db.todos
    .filter()
    .completedEqualTo(false)
    .sortByCreatedAt(order: CindelSortOrder.descending)
    .limit(20)
    .watch()
    .listen((todos) {
      openTodos = todos;
    });
```

## Lazy Watchers

Lazy variants (`watchObjectLazy`, `watchCollectionLazy`, `watchLazy`) emit `void` instead of data. Use them when your app has its own state management layer and you only need an invalidation signal to know when to reload.

```dart theme={null}
// Lazy collection watcher — clear a cache when anything in todos changes.
final sub = db.todos.watchCollectionLazy().listen((_) {
  todoListCache.clear();
});

// Lazy query watcher — invalidate only when the filtered subset may have changed.
final sub = db.todos
    .filter()
    .completedEqualTo(false)
    .watchLazy()
    .listen((_) {
      openTodoCache.invalidate();
    });
```

## Change-Set Watcher

`db.watchCollectionChanges(collectionName)` emits a lower-level `CindelChangeSet` for every committed write to that collection. Use it for advanced cache invalidation, diagnostics, or tooling layers that need to know exactly which document ids changed.

```dart theme={null}
final sub = db.watchCollectionChanges('todos').listen((change) {
  if (change.hasUnknownDocuments) {
    todoCache.clear();
    return;
  }

  for (final id in change.documentIds) {
    todoCache.remove(id);
  }
});
```

`CindelChangeSet` exposes these fields:

| Field                   | Description                                                            |
| ----------------------- | ---------------------------------------------------------------------- |
| `collection`            | The name of the changed collection                                     |
| `documentIds`           | Ids of the documents involved in the change                            |
| `documents`             | The raw changed documents (if available)                               |
| `hasUnknownDocuments`   | `true` when the full change set is not available (e.g. external write) |
| `isExternal`            | `true` when the change originated outside this process                 |
| `revision`              | Database revision after the change                                     |
| `mayAffectDocument(id)` | Returns `true` if the given id may have been affected                  |

Most apps should use typed watchers. Reach for change-set watchers only when you need fine-grained control over a custom cache or diagnostic tool.

## Watcher Lifecycle

<Warning>
  Always cancel watcher subscriptions when the owning widget or state object is disposed. A watcher subscription that outlives its widget continues to process database events and can silently update stale state or cause memory leaks.

  Match the watcher's lifetime to the UI state it drives — a screen-level watcher should be cancelled in `dispose`, and a short-lived component watcher should be cancelled when that component is removed.
</Warning>

The standard pattern is to store the `StreamSubscription` and cancel it on disposal:

```dart theme={null}
late final StreamSubscription<List<Todo>> _sub;

void start() {
  _sub = db.todos
      .filter()
      .completedEqualTo(false)
      .sortByCreatedAt(order: CindelSortOrder.descending)
      .watch()
      .listen((todos) {
        screenState = todos;
      });
}

Future<void> dispose() async {
  await _sub.cancel();
}
```

## Usage Guidance

* **Query watchers** — best default for list screens. Push filtering, sorting, and limits into the query so the stream delivers only what the screen needs.
* **Object watchers** — best for detail and edit screens that track one record. Handle the nullable emission to detect deletion.
* **Lazy watchers** — best when you manage your own cache. The signal is cheap; you decide whether and when to reload.
* **Collection watchers** — convenient for small, bounded collections such as settings or categories.
* **Change-set watchers** — for custom cache layers, diagnostics, and advanced tooling only.

Keep watcher callbacks small. Derive state from the emitted data and avoid long-running work inside a `listen` callback. If a listener triggers asynchronous work, ensure the owning state object can safely ignore stale results after it is disposed.

<Note>
  On Web (SQLite/OPFS), watcher delivery is single-tab only. Writes in one browser tab are not automatically broadcast to watchers in another tab.
</Note>
