chore: reserialize db on equal version, update bindings and docs

- Run de/ser roundtrip in pre_init even when db version matches, ensuring
  all #[serde(default)] fields are populated before any typed access
- Add patchdb.md documentation for TypedDbWatch patterns
- Update TS bindings for CheckPortParams, CheckPortRes, ifconfigUrl
- Update CLAUDE.md docs with patchdb and component-level references
This commit is contained in:
Aiden McClelland
2026-02-16 19:27:48 -07:00
parent cfbace1d91
commit 6a1b1627c5
10 changed files with 128 additions and 2 deletions

View File

@@ -26,7 +26,7 @@ make test-core # Run Rust tests
## Operating Rules
- Always verify cross-layer changes using the order described in [ARCHITECTURE.md](ARCHITECTURE.md#cross-layer-verification)
- Check component-level CLAUDE.md files for component-specific conventions
- Check component-level CLAUDE.md files for component-specific conventions. ALWAYS read it before operating on that component.
- Follow existing patterns before inventing new ones
## Supplementary Documentation

View File

@@ -53,6 +53,8 @@ Patch-DB provides diff-based state synchronization. Changes to `db/model/public.
- `.mutate(|v| ...)` — Deserialize, mutate, reserialize
- For maps: `.keys()`, `.as_idx(&key)`, `.as_idx_mut(&key)`, `.insert()`, `.remove()`, `.contains_key()`
See [patchdb.md](patchdb.md) for `TypedDbWatch<T>` construction, API, and usage patterns.
## i18n
See [i18n-patterns.md](i18n-patterns.md) for internationalization key conventions and the `t!()` macro.
@@ -64,6 +66,7 @@ See [core-rust-patterns.md](core-rust-patterns.md) for common utilities (Invoke
## Related Documentation
- [rpc-toolkit.md](rpc-toolkit.md) — JSON-RPC handler patterns
- [patchdb.md](patchdb.md) — Patch-DB watch patterns and TypedDbWatch
- [i18n-patterns.md](i18n-patterns.md) — Internationalization conventions
- [core-rust-patterns.md](core-rust-patterns.md) — Common Rust utilities
- [s9pk-structure.md](s9pk-structure.md) — S9PK package format

View File

@@ -23,3 +23,4 @@ cd sdk && make baseDist dist # Rebuild SDK after ts-bindings
- When adding RPC endpoints, follow the patterns in [rpc-toolkit.md](rpc-toolkit.md)
- When modifying `#[ts(export)]` types, regenerate bindings and rebuild the SDK (see [ARCHITECTURE.md](../ARCHITECTURE.md#build-pipeline))
- When adding i18n keys, add all 5 locales in `core/locales/i18n.yaml` (see [i18n-patterns.md](i18n-patterns.md))
- When using DB watches, follow the `TypedDbWatch<T>` patterns in [patchdb.md](patchdb.md)

105
core/patchdb.md Normal file
View File

@@ -0,0 +1,105 @@
# Patch-DB Patterns
## Model<T> and HasModel
Types stored in the database derive `HasModel`, which generates typed accessor methods on `Model<T>`:
```rust
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct ServerInfo {
pub version: Version,
pub network: NetworkInfo,
// ...
}
```
**Generated accessors** (one per field):
- `as_version()``&Model<Version>`
- `as_version_mut()``&mut Model<Version>`
- `into_version()``Model<Version>`
**`Model<T>` APIs:**
- `.de()` — Deserialize to `T`
- `.ser(&value)` — Serialize from `T`
- `.mutate(|v| ...)` — Deserialize, mutate, reserialize
- For maps: `.keys()`, `.as_idx(&key)`, `.insert()`, `.remove()`, `.contains_key()`
## Database Access
```rust
// Read-only snapshot
let snap = db.peek().await;
let version = snap.as_public().as_server_info().as_version().de()?;
// Atomic mutation
db.mutate(|db| {
db.as_public_mut().as_server_info_mut().as_version_mut().ser(&new_version)?;
Ok(())
}).await;
```
## TypedDbWatch<T>
Watch a JSON pointer path for changes and deserialize as a typed value. Requires `T: HasModel`.
### Construction
```rust
use patch_db::json_ptr::JsonPointer;
let ptr: JsonPointer = "/public/serverInfo".parse().unwrap();
let mut watch = db.watch(ptr).await.typed::<ServerInfo>();
```
### API
- `watch.peek()?.de()?` — Get current value as `T`
- `watch.changed().await?` — Wait until the watched path changes
- `watch.peek()?.as_field().de()?` — Access nested fields via `HasModel` accessors
### Usage Patterns
**Wait for a condition, then proceed:**
```rust
// Wait for DB version to match current OS version
let current = Current::default().semver();
let mut watch = db
.watch("/public/serverInfo".parse().unwrap())
.await
.typed::<ServerInfo>();
loop {
let server_info = watch.peek()?.de()?;
if server_info.version == current {
break;
}
watch.changed().await?;
}
```
**React to changes in a loop:**
```rust
// From net_controller.rs — react to host changes
let mut watch = db
.watch("/public/serverInfo/network/host".parse().unwrap())
.await
.typed::<Host>();
loop {
if let Err(e) = watch.changed().await {
tracing::error!("DB watch disconnected: {e}");
break;
}
let host = watch.peek()?.de()?;
// ... process host ...
}
```
### Real Examples
- `net_controller.rs:469` — Watch `Hosts` for package network changes
- `net_controller.rs:493` — Watch `Host` for main UI network changes
- `service_actor.rs:37` — Watch `StatusInfo` for service state transitions
- `gateway.rs:1212` — Wait for DB migrations to complete before syncing

View File

@@ -90,7 +90,13 @@ impl Current {
.await
.result?;
}
Ordering::Equal => (),
Ordering::Equal => {
db.apply_function(|db| {
Ok::<_, Error>((to_value(&from_value::<Database>(db.clone())?)?, ()))
})
.await
.result?;
}
}
Ok(())
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type CheckPortParams = { port: number; gateway: GatewayId }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CheckPortRes = { ip: string; port: number; reachable: boolean }

View File

@@ -25,6 +25,7 @@ export type ServerInfo = {
zram: boolean
governor: Governor | null
smtp: SmtpValue | null
ifconfigUrl: string
ram: number
devices: Array<LshwDevice>
kiosk: boolean | null

View File

@@ -56,6 +56,8 @@ export { Category } from './Category'
export { Celsius } from './Celsius'
export { CheckDependenciesParam } from './CheckDependenciesParam'
export { CheckDependenciesResult } from './CheckDependenciesResult'
export { CheckPortParams } from './CheckPortParams'
export { CheckPortRes } from './CheckPortRes'
export { CifsAddParams } from './CifsAddParams'
export { CifsBackupTarget } from './CifsBackupTarget'
export { CifsRemoveParams } from './CifsRemoveParams'

View File

@@ -205,6 +205,7 @@ export const mockPatchData: DataModel = {
caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15',
ntpSynced: false,
smtp: null,
ifconfigUrl: 'https://ifconfig.co',
platform: 'x86_64-nonfree',
zram: true,
governor: 'performance',