pondrs 0.2.5

A pipeline execution library
Documentation
# Node Hooks

Node hooks fire when a runner starts and finishes executing a node, or when a node encounters an error.

## Methods

```rust,ignore
fn before_node_run(&self, n: &dyn PipelineInfo) {}
fn after_node_run(&self, n: &dyn PipelineInfo) {}
fn on_node_error(&self, n: &dyn PipelineInfo, error: &str) {}
```

## Arguments

- **`n`** — the node being executed. Use `n.name()` for the node name, `n.type_string()` for the function's type name.
- **`error`** (on `on_node_error`) — the stringified error message. In `std` builds this is `e.to_string()`; in `no_std` it's the fixed string `"node error"`.

## Lifecycle

For a successful node execution:

```text
before_node_run(n)
  before_dataset_loaded(n, ds0)
  after_dataset_loaded(n, ds0)
  ... (function executes) ...
  before_dataset_saved(n, ds_out)
  after_dataset_saved(n, ds_out)
after_node_run(n)
```

For a failed node:

```text
before_node_run(n)
  ... (error occurs during load, function, or save) ...
on_node_error(n, "error message")
```

`after_node_run` and `on_node_error` are mutually exclusive — exactly one fires per node execution.

## Example: counting nodes

```rust,ignore
use std::sync::atomic::{AtomicUsize, Ordering};

struct NodeCounter {
    count: AtomicUsize,
}

impl Hook for NodeCounter {
    fn after_node_run(&self, n: &dyn PipelineInfo) {
        let i = self.count.fetch_add(1, Ordering::Relaxed) + 1;
        println!("Completed node {} ({}/total)", n.name(), i);
    }

    fn on_node_error(&self, n: &dyn PipelineInfo, error: &str) {
        eprintln!("Node {} failed: {}", n.name(), error);
    }
}
```

## Parallel runner behavior

With the `ParallelRunner`, node hooks may fire from different threads concurrently. This is why `Hook: Sync` is required. Use atomic types or `Mutex` for any shared state in your hook.