diff --git a/CLAUDE.md b/CLAUDE.md index 1b51c4b04..c91a3af4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/core/ARCHITECTURE.md b/core/ARCHITECTURE.md index 701416f12..f895715b2 100644 --- a/core/ARCHITECTURE.md +++ b/core/ARCHITECTURE.md @@ -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` 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 diff --git a/core/CLAUDE.md b/core/CLAUDE.md index b68febba9..fbcbc45a3 100644 --- a/core/CLAUDE.md +++ b/core/CLAUDE.md @@ -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` patterns in [patchdb.md](patchdb.md) diff --git a/core/patchdb.md b/core/patchdb.md new file mode 100644 index 000000000..b35bf221e --- /dev/null +++ b/core/patchdb.md @@ -0,0 +1,105 @@ +# Patch-DB Patterns + +## Model and HasModel + +Types stored in the database derive `HasModel`, which generates typed accessor methods on `Model`: + +```rust +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct ServerInfo { + pub version: Version, + pub network: NetworkInfo, + // ... +} +``` + +**Generated accessors** (one per field): +- `as_version()` — `&Model` +- `as_version_mut()` — `&mut Model` +- `into_version()` — `Model` + +**`Model` 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 + +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::(); +``` + +### 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::(); +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::(); +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 diff --git a/core/src/version/mod.rs b/core/src/version/mod.rs index a470212f5..4c17bc32f 100644 --- a/core/src/version/mod.rs +++ b/core/src/version/mod.rs @@ -90,7 +90,13 @@ impl Current { .await .result?; } - Ordering::Equal => (), + Ordering::Equal => { + db.apply_function(|db| { + Ok::<_, Error>((to_value(&from_value::(db.clone())?)?, ())) + }) + .await + .result?; + } } Ok(()) } diff --git a/sdk/base/lib/osBindings/CheckPortParams.ts b/sdk/base/lib/osBindings/CheckPortParams.ts new file mode 100644 index 000000000..63ab439a7 --- /dev/null +++ b/sdk/base/lib/osBindings/CheckPortParams.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/CheckPortRes.ts b/sdk/base/lib/osBindings/CheckPortRes.ts new file mode 100644 index 000000000..21bf8ce66 --- /dev/null +++ b/sdk/base/lib/osBindings/CheckPortRes.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts index 23ff7ab4b..d8d318016 100644 --- a/sdk/base/lib/osBindings/ServerInfo.ts +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -25,6 +25,7 @@ export type ServerInfo = { zram: boolean governor: Governor | null smtp: SmtpValue | null + ifconfigUrl: string ram: number devices: Array kiosk: boolean | null diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index ae49292ff..1c8b4b839 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -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' diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index a98ea6b85..1f0e7cf59 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -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',