Traces the version gap deferred by the alknet-blobs probe: verifies latest stable iroh 1.0.2 / iroh-blobs 0.103 / irpc 0.17 against crates.io, rules out the 'iroh 0.35 is a stale pin' hypothesis (it's load-bearing — 3 APIs broke), identifies the real cheap win (irpc 0.16 is a dead dep in alknet-call, never imported), and traces the iroh 1.0.x Preset trait so the migration is a concrete 3-edit diff rather than a TODO. Includes the sibling probe doc (alknet-blobs-external-store-probe.md) which the findings cross-references.
228 lines
23 KiB
Markdown
228 lines
23 KiB
Markdown
# alknet-blobs: External SQLite Store — Command-Surface Probe
|
|
|
|
**Status:** Probe complete. The external-crate path (no fork) is feasible. The required upstream change is a ~4-line visibility-widening PR with zero logic changes.
|
|
**Date:** 2026-07-08
|
|
**Scope:** Resolves open question #1 from `poc-summary.md` ("FsStore (redb + filesystem) vs SQLite — the two-database question"). Determines whether a sibling `alknet-blobs` crate can implement a SQLite-backed blob store that speaks iroh-blobs' `Command` actor protocol *without forking iroh-blobs*, by checking whether the full public API surface is sufficient.
|
|
|
|
---
|
|
|
|
## TL;DR
|
|
|
|
The POC summary framed this as "fork iroh-blobs to use SQLite instead of redb — a big maintenance commitment." That framing is a **false dichotomy**. There is a third path the summary didn't consider: implement a `SqliteStore` actor in an external crate that speaks iroh-blobs' public `Command` protocol, with **zero changes to iroh-blobs core** plus a trivial upstream visibility PR.
|
|
|
|
The probe traced every `Command` variant against the published `iroh-blobs 0.103.0` source. Findings:
|
|
|
|
1. **There is no `Store` trait.** `Store` is a concrete struct wrapping an `irpc::Client<proto::Request>` (an mpsc sender). `MemStore` and `FsStore` are two actor implementations that both call `Store::from_sender(sender)`. A third store is a *third actor*, not a trait impl.
|
|
|
|
2. **The `Command` enum is `pub`** (generated by the `irpc` `#[rpc_requests]` macro, which propagates the source `pub enum Request`'s visibility). Every request struct, every channel payload type, and every helper type that crosses the actor boundary (`Hash`, `HashAndFormat`, `Tag`, `TagInfo`, `TempTag`, `Bitfield`, `BlobStatus`, `AddProgressItem`, `ExportProgressItem`, `EncodedItem`, `BaoContentItem`, `Leaf`, `Scope`) is `pub`. A downstream actor can match on `Command` and handle every variant using only pub surface.
|
|
|
|
3. **The only blockers are four `pub(crate)` items** that need widening to `pub`: `Store::from_sender`, `Store::ref_from_sender`, the `ApiClient` type alias, and the `Scope` field. This is a ~4-line, zero-logic upstream PR. Optionally, exposing `TempTags`/`TempTagScope` would enable iroh-native temp-tag tracking, but a self-managed-GC fallback makes this unnecessary.
|
|
|
|
4. **No `Command` variant is BLOCKED.** The redb-specific types (`EntryState`, `DataLocation`, `OutboardLocation`, the redb `tables` module) never cross the pub API boundary — they're confined to the `pub(crate)` `store/fs/meta` module. A SQLite store has its own schema and never touches them.
|
|
|
|
5. **GC is reusable, not re-implementable.** `run_gc` (`store/gc.rs:222`) is `pub` and walks the store via the pub `Store` methods (`tags().list()`, `tags().list_temp_tags()`, `blobs().list()`, `blobs().delete()`, `export_bao()`, `sync_db()`). A downstream store that correctly implements those `Command` variants gets iroh's GC for free.
|
|
|
|
6. **~100 lines of bao-tree orchestration must be re-implemented** (the `ImportBao`/`ExportBao`/`ExportRanges`/`Observe` handlers). This is re-implementation, not reuse — but all of it sits on pub `bao_tree` APIs (`traverse_ranges_validated`, `PreOrderMemOutboard::create`, `BaoTree`, `ChunkRanges`). `MemStore` already demonstrates this pattern: it has its own `BaoFileStorage`/`PartialMemStorage`; `FsStore` has its own; `alknet-blobs` would have its own. One store impl = one copy of the bao glue.
|
|
|
|
**Verdict:** Build `alknet-blobs` as an external crate. Do not fork. Push the 4-line visibility PR upstream. If upstream rejects it, fork — but the fork is ~4 lines different from the external crate, not the "big maintenance commitment" the summary feared.
|
|
|
|
---
|
|
|
|
## Background: Why the POC summary's framing was wrong
|
|
|
|
The POC summary (`poc-summary.md` §"Open Unknowns #1") said:
|
|
|
|
> The alternative — forking iroh-blobs to use SQLite instead of redb — is a big maintenance commitment. The POC's write_chunks table proves SQLite can handle chunk-sized inline BLOBs at iroh's granularity, so the swap is mechanically possible. But it should only be done if the two-database coexistence proves problematic, not for aesthetics.
|
|
|
|
This assumed the choice was binary: (a) run redb + SQLite side-by-side, or (b) fork iroh-blobs to swap redb for SQLite. Reading the actual code reveals a third option: **an external crate that implements the `Command` actor protocol against SQLite, with iroh-blobs untouched.**
|
|
|
|
The key architectural fact the summary missed: **iroh-blobs has no `Store` trait.** `Store` (`src/api.rs:213`) is a concrete struct holding an `irpc::Client<proto::Request>` — a typed mpsc sender. `MemStore` (`src/store/mem.rs`) and `FsStore` (`src/store/fs.rs`) are not trait implementations; they are actor tasks that hold an `mpsc::Receiver<Command>`, handle commands, and produce a `Store` via `Store::from_sender(sender)`. The seam is the `Command` enum, not a trait. A third store is a third actor.
|
|
|
|
This re-frames the risk calculus the summary was conservative about. The "two-database" concern (two WALs, two fsync paths, two crash-recovery stories, silent-corruption risk on the seam) is avoided not by "a big maintenance commitment" but by writing a sibling crate that owns its own single SQLite file — exactly the architecture the POC's path-tree + write-chunks + honker work already validated.
|
|
|
|
---
|
|
|
|
## The Command protocol is pub
|
|
|
|
`iroh-blobs 0.103.0`, `src/api/proto.rs`:
|
|
|
|
- `pub enum Request` (line 92), decorated with `#[rpc_requests(message = Command, alias = "Msg", rpc_feature = "rpc")]` (line 90).
|
|
- The `irpc-derive` macro (`/workspace/irpc/irpc-derive/src/lib.rs:146`) emits `#vis enum #message_enum_name { ... }`, propagating the source enum's `pub`. So **`pub enum Command`** is generated, with one variant per `Request` variant, each wrapping `WithChannels<RequestStruct, Request>`.
|
|
- `WithChannels` fields (`inner`, `tx`, `rx`) are `pub` (`irpc/src/lib.rs:1162-1168`).
|
|
- Per-variant `*Msg` type aliases (e.g. `ImportBaoMsg`, `ExportBaoMsg`) are generated as `pub type` (`irpc-derive/src/lib.rs:328-335`).
|
|
- `From<WithChannels<T, Request>> for Command` impls are generated and pub (`irpc-derive/src/lib.rs:279-281`).
|
|
|
|
Every request struct (`ImportBaoRequest`, `ExportBaoRequest`, `BlobStatusRequest`, `SetTagRequest`, `ListTagsRequest`, …) is `pub` with `pub` fields. Every channel payload type (`Hash`, `HashAndFormat`, `Tag`, `TagInfo`, `TempTag`, `Bitfield`, `BlobStatus`, `AddProgressItem`, `ExportProgressItem`, `EncodedItem`, `BaoContentItem`, `Leaf`, `Scope`, `BatchResponse`, `ImportByteStreamUpdate`, `ExportRangesItem`) is `pub`.
|
|
|
|
The `Command` enum is the actor's inbox type. A downstream crate that spawns a tokio task holding `mpsc::Receiver<Command>` can `match` on every variant and produce a `Store` via `Store::from_sender(sender)` — once that constructor is `pub`.
|
|
|
|
---
|
|
|
|
## The four items that must be made `pub`
|
|
|
|
### Mandatory (the ~4-line PR)
|
|
|
|
| Item | File:line | Current | Needed for |
|
|
|---|---|---|---|
|
|
| `ApiClient` type alias | `src/api.rs:33` | `pub(crate) type ApiClient = irpc::Client<proto::Request>;` | The external store wrapper holds an `ApiClient` field (as `MemStore`/`FsStore` do). Without it the `from_sender` signature can't be expressed. **Alternative:** make `from_sender` take `irpc::Client<proto::Request>` directly and keep `ApiClient` private — fewer surface changes. |
|
|
| `Store::from_sender` | `src/api.rs:299` | `pub(crate) fn from_sender(client: ApiClient) -> Self` | External actor constructs a `Store` from its `mpsc::Sender`. This is the single load-bearing change. |
|
|
| `Store::ref_from_sender` | `src/api.rs:303` | `pub(crate) fn ref_from_sender(client: &ApiClient) -> &Self` | `Deref`/`AsRef<Store>` for the external store wrapper (matches `MemStore`/`FsStore` pattern). |
|
|
| `Scope` field | `src/api/proto.rs:693` | `pub struct Scope(pub(crate) u64);` | `Batch` must *construct* a `Scope` to send back on its `oneshot::Sender<Scope>`; import variants receive a `Scope` they must *destructure*. Only `Scope::GLOBAL` is constructible today. **Minimum:** make the field `pub` (it's a `u64`), or add `Scope::new(u64)`/`Scope::into_inner()`. |
|
|
|
|
### Optional (enables iroh-native temp-tag tracking, NOT required)
|
|
|
|
| Item | File:line | Current | Why |
|
|
|---|---|---|---|
|
|
| `util::temp_tag` module | `src/util.rs:4` | `pub(crate) mod temp_tag;` | `TempTags`, `TempTagScope`, `TagDrop`, `TagCounter` are sealed. Without this, imports can only emit *untracked* temp tags via `TempTag::new(haf, None)` (pub). |
|
|
| `TempTags` struct + `create`/`create_scope`/`end_scope` | `src/util/temp_tag.rs:173` | `pub(crate) struct TempTags` (methods individually `pub` but in sealed module) | Refcounted, GC-aware temp tags for imports and `CreateTempTag`. |
|
|
| `TempTagScope` + `TagDrop` | `src/util/temp_tag.rs:211,60` | sealed | `Batch` scope bookkeeping (`scope.on_drop(&msg)`). |
|
|
| `Bitfield::new_unchecked` | `src/api/proto/bitfield.rs:74` | `pub(crate)` | `ImportBao` fs handler uses it. `Bitfield::new` (pub) is a fine substitute. **Not required.** |
|
|
|
|
**The fallback if `TempTags` stays sealed:** use `TempTag::new(haf, None)` (pub today) for imports. The tag is valid on the wire and protects the blob from GC for the tag's lifetime, but `on_drop` is `None` so the refcount never decrements — effectively a leak until process exit or explicit `DeleteBlobs`. This is acceptable because:
|
|
|
|
1. alknet-blobs owns its SQLite schema; the path tree (from the POC) already tracks which hashes are live via path rows + tags. Self-managed GC against SQLite is the natural design.
|
|
2. `MemStore` and `FsStore` each own their own `TempTags` instance already — there is no shared global temp-tag registry. A third store having its own is the existing pattern, not a deviation.
|
|
3. iroh's `run_gc` (see below) walks the store via pub `Store` methods, so it works regardless of how temp tags are tracked internally — as long as `ListTempTags` returns the right set.
|
|
|
|
Under this fallback, the **only strictly-required upstream change is the 4 lines above.**
|
|
|
|
---
|
|
|
|
## GC is reusable (a correction to the probe's first pass)
|
|
|
|
The first pass of this probe incorrectly concluded that `run_gc` was `pub(crate)` and that a downstream store would have to write its own GC loop. That is wrong:
|
|
|
|
- `pub async fn run_gc(store: Store, config: GcConfig)` — `src/store/gc.rs:222`. It is `pub`.
|
|
- `gc_mark_task` (`gc.rs:34`) walks live hashes via: `store.tags().list()`, `store.tags().list_temp_tags()`, `store.export_bao(hash, ChunkRanges::all()).hashes()`.
|
|
- `gc_sweep_task` (`gc.rs:83`) deletes dead blobs via: `store.blobs().list().stream()`, `store.blobs().delete(batch)`, `store.sync_db()`.
|
|
|
|
Every one of those is a `pub` method on `Store` backed by a `Command` variant (`ListTags`, `ListTempTags`, `ExportBao`, `ListBlobs`, `DeleteBlobs`, `SyncDb`). A downstream `alknet-blobs` store that correctly implements those six variants gets iroh's mark-sweep GC **for free** — it just calls `run_gc(store.clone(), gc_config)` in a spawned task, exactly as `MemStore::new_with_opts` (`mem.rs:143`) and `FsStore::load_with_opts` (`fs.rs:1427`) do.
|
|
|
|
The implication: even under the "self-managed temp tags" fallback, GC is not a thing alknet-blobs has to build. It's a thing it gets by implementing the `Command` variants it would implement anyway.
|
|
|
|
---
|
|
|
|
## Per-variant verdict
|
|
|
|
All ~24 `Command` variants traced. Channel payload types are `pub` for every variant. `EntryState`/`DataLocation`/`OutboardLocation` (redb value types) **never appear in pub channel types** — confirmed by reading every `#[rpc(tx=..., rx=...)]` attribute in `proto.rs`. They are confined to the `pub(crate)` redb meta actor.
|
|
|
|
| Variant | Verdict | Notes |
|
|
|---|---|---|
|
|
| `ListBlobs` | **PUB-OK** | Stream `Hash` from your DB. |
|
|
| `Batch` | **NEEDS-PUB** | Must construct `Scope` for the `oneshot` reply — needs `Scope` field pub. The `TempTagScope::on_drop` bookkeeping is sealed; implement your own ~30-line scope tracker or skip batch-scoped cleanup (acceptable for a SQLite store that owns its own GC). |
|
|
| `DeleteBlobs` | **PUB-OK** | Delete in your DB; protected-set is your own. |
|
|
| `ImportBao` | **PUB-OK (logic-heavy)** | Re-implement ~60-100 lines of bao-tree chunk assembly (`BaoContentItem::{Parent,Leaf}` → outboard + data + `Bitfield`). Uses pub `bao_tree` APIs. See "Re-implementation cost" below. |
|
|
| `ExportBao` | **PUB-OK** | Call pub `traverse_ranges_validated` with a 6-line `Sender` impl you write yourself (the iroh `BaoTreeSender` is sealed but trivial). |
|
|
| `ExportRanges` | **PUB-OK** | ~40 lines of range iteration + `Bitfield` superset checks + `Leaf` emission. Pub `Bitfield`/`Leaf`/`RangeSet2`. |
|
|
| `Observe` | **PUB-OK** | Implement your own `watch::Sender<Bitfield>` → `mpsc::Sender<Bitfield>` forwarder (~15 lines). Pub `Bitfield`. |
|
|
| `BlobStatus` | **PUB-OK** | Return `NotFound`/`Partial`/`Complete` from your DB. |
|
|
| `ImportBytes` | **NEEDS-PUB** | `Scope` field needed (request carries `scope`). Emit `AddProgressItem::Done(TempTag)` via `TempTag::new(haf, None)` (pub, untracked) or `TempTags::create` (sealed). |
|
|
| `ImportByteStream` | **NEEDS-PUB** | Same as `ImportBytes`. |
|
|
| `ImportPath` | **NEEDS-PUB** | Same as `ImportBytes`. |
|
|
| `ExportPath` | **PUB-OK** | Write your data to target path; emit `ExportProgressItem`. |
|
|
| `ListTags` | **PUB-OK** | Query your tag table. |
|
|
| `SetTag` | **PUB-OK** | |
|
|
| `DeleteTags` | **PUB-OK** | |
|
|
| `RenameTag` | **PUB-OK** | |
|
|
| `CreateTag` | **PUB-OK** | `Tag::auto` is pub (`store/util.rs:72`). |
|
|
| `ListTempTags` | **PUB-OK** | Return your own temp-tag list. **Required for `run_gc` to work** — it calls this. |
|
|
| `CreateTempTag` | **NEEDS-PUB** | `Scope` field. `TempTag::new(value, None)` (pub) for untracked, or `TempTags::create` (sealed) for tracked. |
|
|
| `SyncDb` | **PUB-OK** | Flush your SQLite. |
|
|
| `WaitIdle` | **PUB-OK** | Track your own task set. |
|
|
| `Shutdown` | **PUB-OK** | |
|
|
| `ClearProtected` | **PUB-OK** | Your own protected set. |
|
|
|
|
**NEEDS-PUB count:** 6 variants (`Batch`, `ImportBytes`, `ImportByteStream`, `ImportPath`, `CreateTempTag`) — all blocked only by the `Scope` field. One change (`Scope(pub u64)`) unblocks all six.
|
|
|
|
**BLOCKED count:** 0.
|
|
|
|
---
|
|
|
|
## Re-implementation cost (the real maintenance burden)
|
|
|
|
The visibility PR is trivial. The actual work — and the honest maintenance cost — is re-implementing the bao-tree orchestration that the `ImportBao`/`ExportBao`/`ExportRanges`/`Observe` handlers do. These handlers don't wire pub pieces together; they *orchestrate* pub `bao_tree` APIs. A downstream store writes its own copy.
|
|
|
|
| Handler | Lines to re-implement | Uses (all pub) |
|
|
|---|---|---|
|
|
| `ImportBao` | ~60-100 | `BaoContentItem`, `BaoTree`, `ChunkRanges`, `Bitfield::new`, `PreOrderMemOutboard` (for compute-on-import path), `Leaf`. Re-derive the "size-1 leaf validates size" check (`is_validated`, sealed in `pub(crate)` bitfield module — ~3 lines). |
|
|
| `ExportBao` | ~6 | `traverse_ranges_validated` + a `Sender for &mut mpsc::Sender<EncodedItem>` impl (the iroh `BaoTreeSender` is a 6-line `ref_cast` wrapper at `store/util.rs:400-415`). |
|
|
| `ExportRanges` | ~40 | `Bitfield` superset check, `RangeSet2` iteration, `Leaf` emission. |
|
|
| `Observe` | ~15 | `tokio::sync::watch` → `mpsc::Sender<Bitfield>` forwarder. |
|
|
|
|
**Total: ~120-160 lines of bao-tree glue** to re-implement in alknet-blobs. This is the same glue `MemStore` has its own copy of (`src/store/mem.rs`, the `BaoFileStorage`/`PartialMemStorage`/`import_bao`/`export_bao` functions) and `FsStore` has its own copy of (`src/store/fs/bao_file.rs`, 749 lines). The pattern is "one store impl = one copy of the bao glue." alknet-blobs is a third store impl; it gets a third copy. The copy is smaller than FsStore's because a SQLite store doesn't need the partial-blob-on-filesystem lifecycle (the POC's `write_chunks` table handles partial state in SQLite).
|
|
|
|
This is re-implementation, not reuse — but it is re-implementation against *stable, pub `bao_tree` APIs*, not against iroh internals. The maintenance cost is: track `bao_tree` version bumps. That is a much smaller surface than "track all of iroh-blobs."
|
|
|
|
---
|
|
|
|
## alknet-crate reuse for the write/read path
|
|
|
|
The four existing alknet crates (post-POC) reduce some of the work the POC flagged as unknown:
|
|
|
|
- **`alknet-tty`** (`crates/alknet-tty/`) — `TtyHandle` already exposes `tokio::io::AsyncWrite` stdin and `Stream<Item=Bytes>` stdout (`src/backend.rs:139-182`). The `MockStdinSink` (`src/backend.rs:366-403`) is a working `AsyncWrite`-over-`mpsc::Sender<Bytes>` adapter — directly reusable as a pattern for the SFTP write path's chunk bridge into `WriteSession::write_chunk`. The kill-on-Drop contract pattern (`exit_code` future, ADR-056) is the same shape as write-session abort-on-drop.
|
|
- **`alknet-call`** (`crates/alknet-call/`) — already uses `irpc` (workspace dep `irpc = "0.16"`) for structured RPC. The `EventEnvelope`/length-prefixed JSON framing (`src/protocol/wire.rs`) is a different protocol than iroh-blobs' `Command` actor protocol, but the irpc usage pattern (matching on request variants, channel-based replies) is the same shape alknet-blobs' actor will use.
|
|
- **`alknet-core`** (`crates/alknet-core/`) — optional `iroh = "0.35"` dep. **Version mismatch:** alknet-core is on `iroh 0.35`/`irpc 0.16`; published `iroh-blobs 0.103` requires `iroh 1.0`/`irpc 0.17`. alknet-blobs will pull iroh-blobs 0.103's irpc 0.17 transitively. This needs resolution at spec time — either alknet-core bumps to iroh 1.0, or alknet-blobs pins an iroh-blobs version compatible with irpc 0.16. **Open question for the spec.**
|
|
|
|
None of the existing alknet crates implement the blob store's bao-tree orchestration — that is genuinely new work. But the async-IO adapter patterns (AsyncWrite-over-mpsc, Stream-of-Bytes, kill-on-drop futures) are already proven in `alknet-tty` and reduce the write-path wiring risk.
|
|
|
|
---
|
|
|
|
## Versioning reality
|
|
|
|
Three iroh-blobs generations are in play:
|
|
|
|
| Source | Version | iroh | irpc | Notes |
|
|
|---|---|---|---|---|
|
|
| `/workspace/iroh-blobs` (local checkout) | 0.100.0 | 0.98 | 0.14 | Pre-n0-rebrand. Used for the original architecture reading. |
|
|
| crates.io published (cached) | 0.103.0 | 1.0 | 0.17 | What the POC used; what alknet-blobs would target. |
|
|
| alknet-core dep | — | 0.35 | 0.16 | Two majors behind iroh-blobs 0.103. |
|
|
|
|
This probe was run against **0.103.0** (the published version). The visibility findings (`Store::from_sender` pub(crate), `Scope(pub(crate) u64)`, `Command` pub via macro, no `Store` trait, `EntryState` confined to `pub(crate)` meta) all hold on 0.103.0. The 4-line upstream PR would target the 0.103+ line.
|
|
|
|
The alknet-core `iroh 0.35` vs iroh-blobs `iroh 1.0` gap is a spec-time decision, not a probe blocker. It affects which iroh QUIC API the distributed-sync layer (POC 3) binds against, but not the local `Command`-actor surface this probe examined.
|
|
|
|
---
|
|
|
|
## Recommended path
|
|
|
|
1. **Build `alknet-blobs` as a workspace member** (`crates/alknet-blobs/`), not a fork. It depends on `iroh-blobs = "0.103"` (published) and implements the `Command` actor against a single SQLite file (schema: the POC's path-tree tables + write_chunks + a blob-metadata table replacing redb's 4 tables + honker tables, all in one .db).
|
|
|
|
2. **Push the 4-line visibility PR upstream** to n0-computer/iroh-blobs: `Store::from_sender`/`ref_from_sender` + `ApiClient` (or change `from_sender` to take `irpc::Client<proto::Request>` directly) + `Scope` field. Zero logic changes. If merged, alknet-blobs is a pure downstream crate. If rejected, alknet-blobs forks those 4 lines — the fork is 4 lines different from the external crate, which is the opposite of "big maintenance commitment."
|
|
|
|
3. **Use the self-managed-GC fallback** (`TempTag::new(haf, None)` + own SQLite-backed liveness tracking) unless upstream agrees to expose `TempTags`. Reuse iroh's `run_gc` by correctly implementing `ListTempTags`/`ListTags`/`ListBlobs`/`DeleteBlobs`/`ExportBao`/`SyncDb`. This is the same pattern `MemStore`/`FsStore` use (each owns its own `TempTags`); alknet-blobs owns its own, just backed by SQLite.
|
|
|
|
4. **Re-implement the ~120-160 lines of bao-tree orchestration** (`ImportBao`/`ExportBao`/`ExportRanges`/`Observe`) against pub `bao_tree` APIs. Copy the structure from `MemStore` (the simpler reference; `src/store/mem.rs`), not `FsStore` (which has the partial-blob-on-filesystem complexity alknet-blobs doesn't need).
|
|
|
|
5. **Reuse `alknet-tty`'s async-IO adapter patterns** (AsyncWrite-over-mpsc, Stream-of-Bytes, kill-on-drop) for the SFTP write-path bridge into `WriteSession`.
|
|
|
|
6. **Resolve the iroh 0.35 vs 1.0 version gap** at spec time. This is an open question for the architecture doc, not a probe finding.
|
|
|
|
---
|
|
|
|
## What this closes and what remains
|
|
|
|
**Closes:** POC open question #1 ("FsStore (redb + filesystem) vs SQLite — the two-database question"). The answer is: neither. Build a third store (`alknet-blobs`) as an external crate with its own single SQLite file. No fork, no two-database coexistence. The upstream cost is a 4-line visibility PR.
|
|
|
|
**Does not close (deferred to spec):**
|
|
- The iroh 0.35 vs iroh 1.0 version gap (alknet-core dependency alignment).
|
|
- The exact SQLite schema for the blob-metadata tables (the redb 4-table shape is a reference; a SQLite store may unify inline_data/inline_outboard into BLOB columns or split differently — schema design is a spec task).
|
|
- Whether to expose `TempTags` upstream or commit to the self-managed-GC fallback (a spec decision; the fallback works, exposing `TempTags` is a nicety).
|
|
- SFTP wiring (POC open question #3 — unchanged; the `alknet-tty` patterns help but don't close it).
|
|
- The distributed sync layer's binding to iroh 1.0 vs 0.35 (POC 3 used iroh 1.0; alknet-core is on 0.35).
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- Published source probed: `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-blobs-0.103.0/`
|
|
- `src/api.rs:33,213,299,303` — `ApiClient`, `Store`, `from_sender`/`ref_from_sender` (pub(crate))
|
|
- `src/api/proto.rs:693` — `Scope(pub(crate) u64)`
|
|
- `src/api/proto.rs:90-139` — `pub enum Request` + `#[rpc_requests]` (generates `pub enum Command`)
|
|
- `src/store/mem.rs` — reference actor (1127 lines, zero redb)
|
|
- `src/store/fs/meta.rs`, `src/store/fs/meta/tables.rs` — redb confined here (~1100 lines)
|
|
- `src/store/fs/entry_state.rs` — `EntryState`/`DataLocation`/`OutboardLocation` (pub(crate), never in pub channel types)
|
|
- `src/store/gc.rs:222` — `pub async fn run_gc` (reusable, walks store via pub methods)
|
|
- `src/util/temp_tag.rs:85` — `TempTag::new` (pub, untracked fallback); `:173` — `TempTags` (pub(crate))
|
|
- irpc derive macro: `/workspace/irpc/irpc-derive/src/lib.rs:146,279,328` — visibility propagation, `From` impls, `*Msg` aliases
|
|
- POC summary: `docs/research/alknet-filesystem/poc-summary.md` (this doc's parent context)
|
|
- alknet crates surveyed: `crates/alknet-tty/src/backend.rs` (AsyncWrite/Stream/kill-on-drop patterns), `crates/alknet-call/src/protocol/wire.rs` (irpc usage), `crates/alknet-core/Cargo.toml` (iroh 0.35 version gap) |